• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
All guides

Security hardening

The 15-minute audit before you share that URL.

Seven security checks that cover the bugs behind every major vibe-coding breach. Each step takes minutes, works with any AI coding tool, and tells you exactly what to run.

~15 min
9 steps
·
Secrets & keysAuthenticationDatabase access controlPayments & webhooksHeaders & hardening
Security hardening
0/9
complete
Section 01

Secrets & keys

AI assistants put API keys where they don't belong. These checks find them before someone else does.

0/2
complete
  • 01

    Check for secrets in your JavaScript bundle

    Critical2 min

    Open your deployed app, open your browser's developer tools (right-click anywhere → Inspect → Sources tab), and search all files for secret key patterns. Or paste the command below into your terminal.

    API key exposure explained
  • 02

    Audit NEXT_PUBLIC_ / VITE_ prefixed variables

    Critical2 min

    Variables prefixed with NEXT_PUBLIC_ (Next.js), VITE_ (Vite), or EXPO_PUBLIC_ (Expo) are bundled into your client JavaScript — anyone viewing source can read them. That's fine for publishable keys. It's catastrophic for secret keys.

Section 02

Authentication

A login page is not authentication. These checks verify your API actually rejects unauthorized requests.

0/2
complete
  • 03

    Verify API auth is real (not just a login page)

    Critical5 min

    Log in, open your browser's DevTools Network tab, find any API request. Right-click → Copy as cURL. Strip the Authorization header and re-run it. If you still get data back, your auth is cosmetic — the login page exists, but the API behind it has no real protection.

    Broken auth explained
  • 04

    Test if users can see each other's data

    Critical5 min

    Create two accounts. As User A, grab an ID from any API call (watch the Network tab). Log in as User B and request User A's resource by pasting that ID into the URL. If it returns data, you have an IDOR (Insecure Direct Object Reference) — the endpoint looks up records by ID without checking who owns them.

    IDOR explained
Section 03

Database access control

If you're on Supabase or Firebase, your database might be wide open. These checks take 3 minutes.

0/2
complete
  • 05

    Verify Row Level Security is enabled on every table (Supabase)

    Critical3 min

    Your Supabase URL and anon key ship in the client bundle by design — that's normal. The thing actually protecting your data is Row Level Security (RLS): database-level policies that scope every query to the authenticated user. No RLS means anyone who knows your Supabase URL can query every row in every table.

    Supabase security playbook
  • 06

    Check Firebase Security Rules

    Critical2 min

    If you're on Firebase, open the Firebase Console → Firestore → Rules. If you see `allow read, write: if true` — your database is public.

Section 04

Payments & webhooks

If your app has Stripe, verify that payment notifications are verified. Without this, anyone can fake a "payment successful" message.

0/1
complete
  • 07

    Verify Stripe payment notifications are authentic

    High2 min

    Stripe sends your app a webhook when a payment completes. Your handler must verify the Stripe-Signature header before trusting the payload — otherwise anyone can POST a fake "payment successful" event and unlock paid features for free. Run the test below: if your server accepts an unsigned event, it's unprotected.

    Stripe webhook walkthrough
Section 05

Headers & hardening

Security headers tell browsers how to protect your users. They're a 2-minute config change that prevents entire classes of attacks.

0/2
complete
  • 08

    Add security headers

    Medium2 min

    Check your current headers with the curl command below. If nothing comes back, add the four essential headers in your hosting config.

  • 09

    Lock down cross-origin access (CORS)

    Medium2 min

    CORS decides which origins can call your API from the browser. AI defaults to the wildcard (`*`) because it makes local dev painless — and ships straight to production. For anything behind a login, restrict it to your own domain.

Secrets & keys

AI assistants put API keys where they don't belong. These checks find them before someone else does.

Check for secrets in your JavaScript bundle

Open your deployed app, open your browser's developer tools (right-click anywhere → Inspect → Sources tab), and search all files for secret key patterns. Or paste the command below into your terminal.

AI assistants frequently place API keys directly in code that runs in the browser — visible to anyone. If someone finds your OpenAI key, they can spend your money. If they find your Stripe secret key, they can issue refunds.

# Paste this into your terminal — it checks your live site for leaked keys
curl -s https://yourapp.com | grep -oE \
  'sk_live_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9]{30,}|service_role|AKIA[A-Z0-9]{16}'

# Search your local code for secret key patterns
# (service_role = Supabase's all-access backend key, must never be public)
grep -rn 'sk_live_\|sk-\|service_role\|AKIA' --include='*.ts' --include='*.tsx' --include='*.js' src/

Audit NEXT_PUBLIC_ / VITE_ prefixed variables

Variables prefixed with NEXT_PUBLIC_ (Next.js), VITE_ (Vite), or EXPO_PUBLIC_ (Expo) are bundled into your client JavaScript — anyone viewing source can read them. That's fine for publishable keys. It's catastrophic for secret keys.

# List every public-prefixed env var — each one ships to the browser
grep -E '^(NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_)' .env .env.local 2>/dev/null

# Safe to expose (scoped by design):
# NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY

# NEVER expose (full backend access):
# DATABASE_URL, SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY, OPENAI_API_KEY, webhook secrets

Authentication

A login page is not authentication. These checks verify your API actually rejects unauthorized requests.

Verify API auth is real (not just a login page)

Log in, open your browser's DevTools Network tab, find any API request. Right-click → Copy as cURL. Strip the Authorization header and re-run it. If you still get data back, your auth is cosmetic — the login page exists, but the API behind it has no real protection.

AI tools generate login pages that look secure, but the API endpoints behind them often have no real protection. An attacker doesn't use your login page — they call your API directly.

# Step 1: Find an API call in your Network tab
# Step 2: Copy as cURL
# Step 3: Remove the Authorization header and re-run:

curl https://yourapp.com/api/users
# If this returns user data → your API has no auth

curl https://yourapp.com/api/orders
# If this returns orders → anyone can read all orders

Test if users can see each other's data

Create two accounts. As User A, grab an ID from any API call (watch the Network tab). Log in as User B and request User A's resource by pasting that ID into the URL. If it returns data, you have an IDOR (Insecure Direct Object Reference) — the endpoint looks up records by ID without checking who owns them.

← Previous guide

Prototype to production

Database, auth, hosting — every step from demo to deployed.

Next guide →

Launch day playbook

Custom domain to live URL — the final mile before you share it.

Done with the guide?

A checklist tells you what to do. A scan proves you did it. Paste your URL and verify everything in minutes.

Run a free scanBrowse all guides

IDOR is the #1 web vulnerability. AI generates endpoints that fetch by ID without checking who's asking. If User A's order is at /api/orders/123, User B just tries /api/orders/123 and gets it.

# As User B, try to fetch User A's data:
curl https://yourapp.com/api/orders/USER_A_ORDER_ID \
  -H "Authorization: Bearer USER_B_TOKEN"

# If this returns User A's order → anyone can read anyone's data
# Fix: every database query must filter by the logged-in user's ID

Database access control

If you're on Supabase or Firebase, your database might be wide open. These checks take 3 minutes.

Verify Row Level Security is enabled on every table (Supabase)

Your Supabase URL and anon key ship in the client bundle by design — that's normal. The thing actually protecting your data is Row Level Security (RLS): database-level policies that scope every query to the authenticated user. No RLS means anyone who knows your Supabase URL can query every row in every table.

In our scan of 100 vibe-coded apps, 68% of Supabase apps had missing or broken RLS. Without it, anyone who knows your Supabase URL can query your database directly and read everything.

-- In your Supabase dashboard → SQL Editor → run:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';

-- Every row where rowsecurity = false is a wide-open table.

-- Fix: enable RLS and add a policy scoped to the authenticated user
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users read own rows" ON your_table
  FOR SELECT USING (auth.uid() = user_id);

Check Firebase Security Rules

If you're on Firebase, open the Firebase Console → Firestore → Rules. If you see `allow read, write: if true` — your database is public.

// ❌ This means your database is wide open:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

// ✅ Fix: require authentication
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;
    }
  }
}

Payments & webhooks

If your app has Stripe, verify that payment notifications are verified. Without this, anyone can fake a "payment successful" message.

Verify Stripe payment notifications are authentic

Stripe sends your app a webhook when a payment completes. Your handler must verify the Stripe-Signature header before trusting the payload — otherwise anyone can POST a fake "payment successful" event and unlock paid features for free. Run the test below: if your server accepts an unsigned event, it's unprotected.

# Send a fake Stripe event — this SHOULD be rejected
curl -X POST https://yourapp.com/api/webhooks/stripe \
  -H "Content-Type: application/json" \
  -d '{"type":"checkout.session.completed","data":{"object":{"id":"cs_fake"}}}'

# If your server returns 200 → your webhook is unprotected

# Fix: verify the signature
import Stripe from 'stripe';
const event = stripe.webhooks.constructEvent(
  body,
  request.headers['stripe-signature'],
  process.env.STRIPE_WEBHOOK_SECRET // server-side only
);

Headers & hardening

Security headers tell browsers how to protect your users. They're a 2-minute config change that prevents entire classes of attacks.

Add security headers

Check your current headers with the curl command below. If nothing comes back, add the four essential headers in your hosting config.

# Check current headers
curl -sI https://yourapp.com | grep -iE \
  'x-frame|content-type-options|strict-transport|content-security'

# Add these in vercel.json, _headers (Netlify), or next.config.js:
# X-Frame-Options: DENY                          → prevents clickjacking
# X-Content-Type-Options: nosniff                → prevents MIME sniffing
# Strict-Transport-Security: max-age=31536000    → forces HTTPS
# Referrer-Policy: strict-origin-when-cross-origin → prevents URL leakage

Lock down cross-origin access (CORS)

CORS decides which origins can call your API from the browser. AI defaults to the wildcard (`*`) because it makes local dev painless — and ships straight to production. For anything behind a login, restrict it to your own domain.

# Check your CORS headers:
curl -sI -H "Origin: https://evil.com" https://yourapp.com/api/users | grep -i access-control

# If you see Access-Control-Allow-Origin: * → fix it
# Only allow your real domains:
# Access-Control-Allow-Origin: https://yourapp.com