# Vibe Code Security Scanner: Leaked API Keys, Supabase RLS (`magnova/vibe-code-security-scanner`) Actor

Scan any app built with AI (Codex, Cursor, Claude Code, Lovable, Bolt, v0, Replit) by URL for leaked OpenAI, Claude, Gemini and 60+ other API keys, open Supabase tables, source maps and missing headers. A-F grade, fix prompts. $0.05/site.

- **URL**: https://apify.com/magnova/vibe-code-security-scanner.md
- **Developed by:** [Magnova](https://apify.com/magnova) (community)
- **Categories:** Developer tools, AI, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $35.00 / 1,000 site scanneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Vibe Code Security Scanner: leaked API keys, open Supabase tables, exposed source maps

Paste the URL of any app you built with AI (**Codex, Cursor, Claude Code, Windsurf, Copilot, Lovable, Bolt, v0, Replit, Base44** or anything else) and get an A–F security grade in seconds. The scanner reads exactly what every visitor's browser downloads (your HTML, every JavaScript bundle and lazy-loaded chunk, and any public source maps) and tells you which secrets are leaking, whether your database (Supabase, Firebase, Base44, Appwrite, PocketBase, Convex, Hasura/Nhost or Parse) can be read by strangers, and how to fix each problem with a copy-paste prompt for your AI coding tool.

**$0.05 per site scanned. No sign-up beyond Apify, no GitHub access, no code upload.**

### What it finds

| Check | What it means for you |
|---|---|
| **Leaked AI keys (any model)** | OpenAI, Anthropic (Claude), Google Gemini, xAI (Grok), Mistral, DeepSeek, Groq, OpenRouter, Perplexity, Cohere, Together, Fireworks, Cerebras, NVIDIA NIM, Hugging Face, Replicate, ElevenLabs, Deepgram, AssemblyAI, fal.ai, Azure OpenAI, Qwen, Kimi, Pinecone, LangSmith, Voyage and 30+ more. Keys with a known prefix are matched directly; prefix-less keys (Mistral, Cohere, DeepSeek...) are caught by the variable they are assigned to (`VITE_MISTRAL_API_KEY = ...`). |
| **AI calls made from the browser** | Front-end code that calls api.openai.com, api.anthropic.com, generativelanguage.googleapis.com and 25+ other AI endpoints with an auth header, plus the `dangerouslyAllowBrowser: true` flag: a key is in the browser even when it is assembled at runtime. |
| **Other leaked secrets** | Stripe secret/restricted, Square, PayPal/Braintree, Polar, Razorpay, Shopify, Supabase `service_role` and `sb_secret_`, AWS, Google OAuth, GitHub, GitLab, npm, PyPI, Vercel, DigitalOcean, Doppler, PlanetScale, Sentry, Slack and Discord (tokens and webhooks), Telegram bots, SendGrid, Resend, Mailgun, Mailchimp, Brevo, Postmark, Twilio, Clerk, Notion, Airtable, Linear, Mapbox secret, PostHog personal keys, private keys, Postgres/MongoDB/MySQL/Redis connection strings, and high-entropy strings assigned to `secret`/`token` variables |
| **Public-by-design keys (no false alarms)** | Stripe `pk_`, Supabase anon / `sb_publishable_`, Firebase web config and referrer-restricted Google Maps keys are labelled *public by design* with an explanation, instead of a scary "LEAK!" |
| **JWT decoding** | Every JWT is decoded so we can tell a harmless Supabase `anon` key from a catastrophic `service_role` key (role, project ref, expiry are shown) |
| **Open Supabase tables** *(owner mode)* | Using only the anon key your app already ships, the scanner checks each table read-only and reports which ones anyone on the internet can read, with column names and sensitive-looking columns (email, phone, token, address). Works with legacy anon keys and new `sb_publishable_` keys. Since 2026 Supabase hides the table list from public keys, so tables are found from your code, from GraphQL introspection when pg_graphql is on, from ~80 common table names, and from any names you add in `extraTables`. Private-looking Storage buckets (signed-URL or per-user folders) are flagged high. This is the Row Level Security mistake behind the 2025 Lovable data leaks |
| **Open Firebase** *(owner mode)* | Realtime Database world-readable rules, Firestore collections left in test mode, Storage buckets anyone can list |
| **Supabase alternatives** *(owner mode)* | **Base44** entities, **Appwrite** collections (permissions granting `any`), **PocketBase** collections with an empty List rule, **Convex** public queries that return personal data, **Hasura / Nhost** GraphQL tables readable (or writable) without login, **Parse / Back4App** classes incl. `_User`, and **Supabase Storage** buckets whose file list is public. Endpoints, project IDs and table names are read from your own bundle |
| **Smart severity (no false alarms)** | A readable table of blog posts or products is reported as *low* ("fine if it's meant to be public"); one with emails, phones, addresses, names or tokens, or named users/orders/payments/messages, is *critical*. Values are checked in memory only and never stored |
| **Database & backend secrets** | Supabase `service_role`/`sb_secret_`/management tokens, Turso/libSQL tokens (JWT decoded: read-write vs read-only), Neon, Xata, Appwrite server keys, Convex deploy keys, Upstash/Vercel KV tokens, Nhost/Hasura admin secrets, InstantDB admin tokens, Parse master keys, Prisma Accelerate URLs, Firebase/Google service-account JSON, and app secrets like `JWT_SECRET`, `NEXTAUTH_SECRET`, `AUTH_SECRET`. Tutorial strings (`postgres:postgres@localhost`, `[YOUR-PASSWORD]`) are ignored |
| **Exposed files** *(owner mode)* | `/.env`, `/.env.local`, `/.env.production`, `/.git/config`, `/.git/HEAD`, `/.DS_Store`, `/backup.zip` — confirmed by file signature, so single-page-app catch-all routes don't cause false positives |
| **Source-map reconstruction** | Public `.map` files are parsed, the original file names are listed (for example `src/lib/supabase.ts`) and the original source is scanned for secrets |
| **Full bundle coverage** | Follows `<script>` tags, `modulepreload` links, Next.js build manifests and app-router chunks, and Vite/webpack lazy chunks — not just the first bundle |
| **Security headers** | Content-Security-Policy, HSTS, X-Frame-Options / frame-ancestors, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, weak CSP, X-Powered-By |
| **Transport** | HTTPS, HTTP→HTTPS redirect, TLS certificate validity and expiry, mixed content |
| **Cookies and CORS** | Session cookies missing Secure/HttpOnly/SameSite, CORS that reflects any origin or allows `*` with credentials |
| **Platform fingerprint** | Lovable, Bolt, Replit, Base44, Vercel, Netlify, Cloudflare Pages, Firebase Hosting; Next.js, Vite, CRA, SvelteKit, Nuxt; Supabase, Firebase, Base44, Appwrite, PocketBase, Convex, Hasura, Nhost, Turso, Neon, Upstash, Xata, InstantDB, PlanetScale, Parse/Back4App, MongoDB, Prisma Accelerate, Fauna, Clerk |

### Fixes you can paste, not just warnings

Every finding comes with:

- **fixPlainEnglish** — what is wrong and what to do, written for builders, not pentesters.
- **fixPrompt** — a ready prompt for whatever AI coding tool you use (Codex, Cursor, Claude Code, Lovable, Bolt, Replit...), e.g. *"Move every call to the AI provider into a server-side API route that reads the key from a server-only env var..."*
- **fixConfig** — the exact file to drop in: `vercel.json`, Netlify/Cloudflare `_headers`, `next.config.js`, `firebase.json`, Supabase RLS SQL, Firebase rules, or the Vite/Next setting that turns off public source maps.

Each site row also has a **headerConfig** with every missing header for your host in one snippet.

### How to use it

1. Put one or more app URLs in **App URLs to scan**. The homepage is enough.
2. Leave **I own these sites** off for a passive scan of anything, or turn it on for your own apps to add the database and exposed-file checks.
3. Click **Start**. Most sites finish in 2–30 seconds.

#### Input example

```json
{
  "urls": ["https://myapp.lovable.app"],
  "iOwnThisSite": true
}
```

#### Output example (one row per site)

```json
{
  "url": "https://myapp.lovable.app",
  "status": "ok",
  "score": 18,
  "grade": "F",
  "critical": 2,
  "high": 0,
  "medium": 3,
  "topIssue": "Supabase table 'profiles' is publicly readable (~1342 rows)",
  "platform": "lovable",
  "framework": "Vite",
  "backends": ["Supabase"],
  "jsFilesScanned": 12,
  "sourceMapsFound": 0,
  "findings": [
    {
      "severity": "critical",
      "category": "database",
      "title": "Supabase table 'profiles' is publicly readable (~1342 rows)",
      "note": "Anyone with the public anon key can read this table... Sensitive-looking columns: email, phone.",
      "fixPrompt": "Paste into Lovable: \"In Supabase, enable Row Level Security on the public.profiles table...\"",
      "fixConfig": "alter table public.profiles enable row level security; ..."
    },
    {
      "severity": "info",
      "title": "Supabase anon key (public by design)",
      "maskedEvidence": "eyJh...64p0 (208 chars)",
      "publicByDesign": true,
      "jwtClaims": {"role": "anon", "ref": "abcd...", "iss": "supabase"}
    }
  ]
}
```

Try it on our deliberately vulnerable demo app (all keys are fake): the prefilled URL in the input.

### Scan on every deploy (GitHub Action)

Add your Apify API token as the repository secret `APIFY_TOKEN`, then save this as `.github/workflows/security-scan.yml`. The build fails if any critical issue is found.

```yaml
name: Vibe code security scan
on:
  deployment_status:
  workflow_dispatch:
jobs:
  scan:
    if: github.event_name == 'workflow_dispatch' || github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - name: Scan deployed app
        env:
          APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
          APP_URL: ${{ github.event.deployment_status.environment_url || 'https://myapp.vercel.app' }}
        run: |
          curl -s -X POST "https://api.apify.com/v2/acts/magnova~vibe-code-security-scanner/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
            -H 'Content-Type: application/json' \
            -d "{\"urls\": [\"$APP_URL\"]}" > report.json
          jq -r '.[] | "\(.grade) \(.score)/100  critical=\(.critical) high=\(.high)  \(.url)"' report.json
          jq -r '.[].findings[]? | select(.severity=="critical" or .severity=="high") | "- [\(.severity)] \(.title)"' report.json
          test "$(jq '[.[].critical // 0] | add' report.json)" -eq 0
```

### Monitor your apps on a schedule

1. Create an Apify **Schedule** (daily or weekly) for this Actor with your app URLs.
2. Pick a **History store** (any key-value store in your account, e.g. `app-security-history`).
3. Turn on **Only report new findings**.

Each run marks findings as `new` or `unchanged` and lists `fixedSinceLastScan`, so you only hear about regressions. Connect the run to Slack, email or a webhook in the Actor's **Integrations** tab. You can also trigger it from a **Vercel deploy hook** or Netlify build hook via Apify webhooks / the run API above.

### For agencies and builders with many apps

Put all your client apps in the `urls` list. Each site is one row with grade, counts and top issue, so the dataset's **Security report** view is a ready portfolio table. Export to CSV, Excel or Google Sheets.

### Use with AI agents / MCP

The Actor has one required input and returns structured JSON, so Claude, ChatGPT, Cursor and other agents can call it through the Apify MCP server (`mcp.apify.com`) zero-shot:

```json
{"urls": ["https://example-app.vercel.app"]}
```

Agents get `grade`, `score`, `topIssue` and `findings[].fixPrompt`, which they can apply directly to your codebase.

### How much does it cost to scan a website for leaked API keys?

**$0.05 per site scanned** (pay per event, no start fee, no monthly rental). Higher Apify plans get automatic discounts (Silver 15% off, Gold and above 30% off). Invalid URLs, unreachable sites and sites skipped because of your spending limit are free. Scanning 100 apps costs $5.

### Vibe Code Security Scanner API

Call it from any language through the Apify API: `POST https://api.apify.com/v2/acts/magnova~vibe-code-security-scanner/run-sync-get-dataset-items?token=YOUR_TOKEN` with the JSON input above. Python and JavaScript clients (`apify-client`) work too.

### FAQ

#### How do I check if my Lovable app is secure?

Paste your `*.lovable.app` (or custom domain) URL, tick **I own these sites**, and run. The most important result is whether any Supabase table is publicly readable; the fix prompt tells Lovable exactly which RLS policies to add.

#### Does it work with Supabase alternatives like Appwrite, PocketBase, Convex or Base44?

Yes. Owner mode tests Base44, Appwrite, PocketBase, Convex, Hasura/Nhost and Parse/Back4App the same way it tests Supabase: it finds the endpoint and table names in your bundle, makes read-only requests without logging in, and tells you which data strangers can see, with a fix prompt for each. For Turso, Neon, Upstash, Xata, InstantDB, PlanetScale and MongoDB it catches leaked database URLs, tokens and admin keys.

#### Is my Supabase anon key a leak?

No. It is meant to be public, and the scanner labels it so. It is only dangerous when Row Level Security is off, which is what owner mode tests.

#### How do I know if my OpenAI, Claude or Gemini key is exposed?

Any OpenAI, Anthropic (Claude), Gemini, Grok, Mistral, DeepSeek, Stripe secret (`sk_live_`) or similar key found in your HTML, JavaScript chunks or source maps is reported as critical, with where it was found (file URL or original file name) and a masked value.

#### Does it attack my site?

No. Default mode only reads files a normal browser downloads. Owner mode adds a few read-only requests (one `limit=1` read per table or collection on the backend your app uses, GraphQL *queries* only, Convex *queries* only, single GETs for files like `/.env`). It never writes, deletes, fuzzes, logs in or brute-forces anything, and never uses a secret key it finds.

#### Is it legal to scan a website?

Passive checks read only public files any visitor's browser receives. Owner mode is for sites you own or have written permission to test; you confirm that with the checkbox. Do not use it against systems you are not authorised to test.

#### Do you store my data?

Findings are written only to your own run's dataset. Secret values are masked (first and last 4 characters), database row values are discarded immediately (only column names are reported) and nothing is kept by us.

#### Why is my grade low when I'm on lovable.app or bolt.host?

On platform subdomains the platform controls response headers, so header findings there aren't your fault yet. They become yours to fix (with the provided config) once you move to a custom domain. Header issues are capped at 10 points, so a site whose only issues are missing headers still gets an A (90), and they never outweigh a real leak.

#### What does "inconclusive" mean?

If a database answers a read test in a way the scanner doesn't recognise (a server error, a rate limit, an unexpected response format), that check is reported as **inconclusive**, never as a pass. The result shows `inconclusiveChecks` above 0, `allClear` false, and a `diagnostics` list with only the backend name, a generic endpoint template, the HTTP status and the response shape. No data, URLs or keys are in it, so it's safe to paste into an issue. If a rescan gives the same result, please open an issue on this Actor's Issues tab with that `diagnostics` block and we'll fix support for your setup.

#### Can it miss things?

Yes. It sees the deployed front end, not your server code, private API routes or business logic (e.g. broken authorisation on `/api/orders/2`). Use it as a fast, repeatable first line, not a full penetration test.

### Alternatives

Free web scanners give a one-off report in a browser tab. This Actor is built for automation: an API, bulk URLs, scheduled monitoring with new/fixed tracking, CI integration and MCP access for AI agents, at $0.05 per scan with no subscription.

### Legal and data

- **Data source:** only the public HTML, JavaScript, source maps and HTTP headers that the target site serves to any visitor; owner-mode probes use only keys the site itself publishes, read-only.
- **Authorisation:** owner-only checks require the user to confirm ownership or permission (`iOwnThisSite`). Scanning localhost and private network addresses is blocked.
- **Privacy:** secrets are masked; database row contents are never output or stored; no personal data is collected.
- The user is responsible for having permission to test the sites they scan.

# Actor input Schema

## `urls` (type: `array`):

Deployed web app URLs to scan (Lovable, Bolt, Replit, v0, Vercel, Netlify, Cloudflare Pages, Firebase Hosting or any site built with Cursor / Claude Code). One site per URL; the homepage is enough, the scanner follows the app's own JavaScript bundles and lazy chunks. Only scan sites you own or are allowed to test. Example: https://myapp.lovable.app

## `iOwnThisSite` (type: `boolean`):

Tick only for sites you own or have written permission to test. Adds READ-ONLY checks of whether strangers can read your data: Supabase tables (RLS) and Storage, Firebase Realtime Database / Firestore / Storage, Base44 entities, Appwrite collections, PocketBase collections, Convex queries, Hasura/Nhost GraphQL and Parse/Back4App classes, plus exposed files like /.env and /.git/config. Nothing is ever written or deleted, row values are never stored, and secret keys found are never used.

## `extraTables` (type: `array`):

Owner mode only. Newer Supabase projects hide the table list from the public key, and minified builds can hide table names, so add any table names you know (e.g. profiles, orders). Each is tested read-only for public access, on top of the names found in your code and a list of common names.

## `historyStore` (type: `string`):

Optional. Pick or create a key-value store in your account (e.g. one named app-security-history). The scanner remembers each site's findings there and marks every finding as new or unchanged, and lists what was fixed since the last scan. Use it with an Apify Schedule or a deploy hook.

## `onlyNewFindings` (type: `boolean`):

With a history store picked, only list findings that were not present in the previous scan of that site (the score and fixed list are still reported). Handy for scheduled or per-deploy scans.

## `maxJsFiles` (type: `integer`):

How many same-site JavaScript files (entry bundles plus lazy-loaded chunks) to read per site. Big Next.js apps can have 100+ chunks. Example: 60.

## `checkSourceMaps` (type: `boolean`):

Download public .map files, list the original source files they expose and scan the original code for secrets.

## `maxTables` (type: `integer`):

Owner mode only: upper limit of Supabase tables to test for public read access.

## Actor input object example

```json
{
  "urls": [
    "https://api.apify.com/v2/key-value-stores/7L3bYX0XdtOUdPP2s/records/index.html"
  ],
  "iOwnThisSite": false,
  "extraTables": [],
  "onlyNewFindings": false,
  "maxJsFiles": 60,
  "checkSourceMaps": true,
  "maxTables": 40
}
```

# Actor output Schema

## `report` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://api.apify.com/v2/key-value-stores/7L3bYX0XdtOUdPP2s/records/index.html"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("magnova/vibe-code-security-scanner").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": ["https://api.apify.com/v2/key-value-stores/7L3bYX0XdtOUdPP2s/records/index.html"] }

# Run the Actor and wait for it to finish
run = client.actor("magnova/vibe-code-security-scanner").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://api.apify.com/v2/key-value-stores/7L3bYX0XdtOUdPP2s/records/index.html"
  ]
}' |
apify call magnova/vibe-code-security-scanner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,magnova/vibe-code-security-scanner"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/FXeIdd0EvwANhUdAw/builds/B8JDSvbljceHMHyfc/openapi.json
