Arvind Verma

How to launch a vibe-coded app securely: a step-by-step guide

Take an app built with Claude Code, Cursor, Lovable or Bolt live safely: find leaked keys and open data, deploy, add Cloudflare, test and monitor.

By Arvind Verma12 min read

In short

  • AI-built apps break in the same places: keys in the browser bundle, tables without RLS, and routes that never check who owns a record.
  • Run deterministic scanners for secrets, dependencies and code first, then an agent review, then read the auth and payment code yourself.
  • Put Cloudflare in front, set security headers, test from outside, and add uptime, error and spend alerts before the first user signs up.
On this page
  1. The failure patterns to fix first
  2. Pre-launch review: scanners first, then agents, then you
  3. Environments and secrets
  4. Choosing where to deploy
  5. If you run your own server
  6. Put Cloudflare in front
  7. Security headers and cookies
  8. After deploy: test from outside
  9. Monitoring and alerts
  10. Backups and a simple incident plan
  11. Launch checklist

Building an app quickly with Claude Code, Codex, Cursor, Lovable, Bolt, v0, Replit Agent or a similar tool is one step for founders, indie hackers and developers. Putting real users on it without leaking keys, data or money is the next. You get, in launch order, the failure patterns that keep turning up in AI-generated apps and how to check each one, the review tools to run before launch, a deployment path with Cloudflare in front, the checks to run once it is live, and a launch checklist to copy.

I take apps like this live and harden them for clients through NoDowntime Technologies. The tools build features fast. Deciding who may see which record, where the keys live and what a stranger can do with a thousand requests a minute is still your job.

The failure patterns to fix first

Most of these map to the OWASP Top 10:2025 (A01 Broken Access Control, A02 Security Misconfiguration, A03 Software Supply Chain Failures) and the OWASP API Security Top 10 (API1 Broken Object Level Authorization, API4 Unrestricted Resource Consumption).

Secrets in the browser bundle or the repo

Next.js inlines any variable prefixed NEXT_PUBLIC_ into the JavaScript sent to the browser at build time. Vite does the same with VITE_, and its docs say those variables should not contain API keys. The browser may hold publishable keys only. Supabase’s secret key (and the legacy service_role key) bypasses Row Level Security and must never reach a browser, a shipped app or source control.

# Any tracked .env file is a leak, even if .gitignore lists it now
git ls-files | grep -iE '(^|/)\.env'

# Every variable name that reaches the browser: read the list
grep -rhoE '(NEXT_PUBLIC|VITE)_[A-Z0-9_]+' --exclude-dir=node_modules --exclude-dir=.git . | sort -u

Supabase tables without RLS, Firebase rules left open

Supabase’s docs are blunt: “A table in an exposed schema without RLS is readable and writable by any role with a grant on it.” Its Security Advisor flags this as an error (“Table publicly accessible”), and supabase db advisors runs the same checks from the CLI. Find and fix:

-- Tables in the public schema with RLS off
select tablename from pg_tables
where schemaname = 'public' and rowsecurity = false;

alter table public.invoices enable row level security;

create policy "Owners can read their invoices"
on public.invoices for select
to authenticated
using ((select auth.uid()) = user_id);

On Firebase, read the rules before launch. Firebase says never to use allow read, write: if true; in production, test mode grants access to all users, and if request.auth.uid != null lets any signed-in user read and write the whole database. Compare request.auth.uid with the document’s owner instead.

Public storage buckets

A public Supabase bucket lets anyone with a file’s URL download it; private buckets go through RLS or time-limited signed URLs. New S3 buckets do not allow public access by default, and AWS recommends keeping all four Block Public Access settings on. Invoices, ID scans and other user uploads belong in a private bucket.

Routes that never check ownership (IDOR)

OWASP describes it as “viewing or editing someone else’s account by providing its unique identifier”. The usual shape: a handler checks a session exists, then loads whatever ID arrived in the URL:

// Before: any signed-in user can read any invoice by changing the ID
const invoice = await db.invoice.findUnique({ where: { id } });

// After: the query itself is scoped to the owner
const invoice = await db.invoice.findFirst({
  where: { id, ownerId: session.user.id },
});
if (!invoice) return new Response("Not found", { status: 404 });

To check, create two test accounts. Signed in as the second, request the first account’s IDs on every GET, PATCH and DELETE route, and call admin routes as a normal user (API5, Broken Function Level Authorization). Anything other than 403 or 404 is a bug.

No rate limits, and unbounded AI spend

API4 warns that requests not limited in compute or paid third-party services “can enable denial of service or drive up costs”. Rate limit sign-up, login, password reset, OTP, checkout and every route that calls a model, per user and per IP, and cap input size and output tokens on AI routes.

Then set a ceiling at the provider. On the OpenAI platform, open the project’s Limits page, edit the monthly spend limit and turn on Enforce a hard limit; without it, alerts only notify. Over the limit, requests return a 429 (project_spend_limit_exceeded), though spend can slightly overshoot. In the Claude Console, set a monthly limit and email alerts under Spend limits on the Workspaces page; the Default Workspace cannot take one, so create a workspace per app and environment.

Permissive CORS

Per MDN, the wildcard * works only for requests without credentials, so some code reflects any Origin instead, which is just as open. Check an allow-list, echo the matched origin and send Vary: Origin.

curl -sI -H "Origin: https://attacker.example" https://api.example.com/api/me | grep -i access-control

If that echoes attacker.example with Access-Control-Allow-Credentials: true, fix it.

Debug mode, stack traces and default credentials

OWASP A02 lists error handling that “reveals stack traces or other overly informative error messages” and default accounts “still enabled and unchanged”. Django says never to deploy with DEBUG on; Flask’s debugger “allows executing arbitrary Python code from the browser”. Remove seeded admin users and demo passwords, and change every default password.

Hallucinated and typosquatted packages

A USENIX Security 2025 paper by Spracklen and colleagues tested 16 code-generating models across 576,000 samples. On average at least 5.2% of packages from commercial models and 21.7% from open-source models did not exist: 205,474 unique invented names. Anyone can register one and wait, an attack now known as slopsquatting. Before installing a package an agent added, confirm it is real:

npm view PACKAGE_NAME repository.url time.created

A missing repository, a creation date from last week or a name one letter off a popular package means stop. Commit the lockfile and install with npm ci in CI.

Pre-launch review: scanners first, then agents, then you

Deterministic scanners

These give the same answer every run, so they can gate CI. The dependency scanning comparison and the SAST tools guide cover the choices in depth.

# Secrets in git history and in the working tree
gitleaks git -v
gitleaks dir -v .
# TruffleHog checks whether found keys are live (run from the parent directory)
trufflehog git file://my-app --results=verified

# Known-vulnerable dependencies
npm audit --audit-level=high
osv-scanner scan source -r .

# Static analysis; --error exits 1 on findings
semgrep scan --config auto --error

--config auto fetches rules from the Semgrep Registry and sends your project URL to do it. On GitHub, CodeQL default setup is under Settings > Advanced Security > CodeQL analysis > Set up > Default, for public repositories or those with GitHub Code Security.

For performance and accessibility, run Lighthouse (npm install -g lighthouse, Node 22 or later) against staging, then write one Playwright smoke test per critical path: sign-up, login, payment and the main feature.

// tests/smoke.spec.ts
import { test, expect } from "@playwright/test";

const BASE = process.env.BASE_URL ?? "http://localhost:3000";

test("sign up and reach the dashboard", async ({ page }) => {
  await page.goto(`${BASE}/signup`);
  await page.getByLabel("Email").fill(`smoke+${Date.now()}@example.com`);
  await page.getByLabel("Password").fill(process.env.SMOKE_PASSWORD ?? "");
  await page.getByRole("button", { name: "Create account" }).click();
  await expect(page).toHaveURL(/dashboard/);
});
npm init playwright@latest
BASE_URL=https://staging.example.com npx playwright test

Test payments on staging in a Stripe sandbox with test keys and card 4242 4242 4242 4242; test cards do not work in live mode.

Agent review in Claude Code and Codex

In Claude Code, two review commands are built in:

/security-review
/code-review

/security-review reviews the diff between your branch and origin’s default branch for injection, auth issues and data exposure, so it covers your branch’s changes, not code already on main. /code-review checks the current diff for correctness bugs; --fix applies the findings. For more depth, install from Anthropic’s official marketplace:

/plugin install security-guidance@claude-plugins-official
/plugin install claude-security@claude-plugins-official
/claude-security scan my branch

The security guidance plugin reviews Claude’s own edits as it writes them. The Claude Security plugin scans a whole repository or a diff with independent verification of each finding, and needs a paid plan or API access. Trail of Bits publishes review plugins for both agents, such as insecure-defaults (fail-open configuration) and differential-review:

/plugin marketplace add trailofbits/skills
/plugin install insecure-defaults@trailofbits

In Codex, type /review in a session, or run it from the shell:

codex review --base main
codex plugin marketplace add trailofbits/skills
codex plugin add insecure-defaults@trailofbits

Cloudflare’s security audit skill installs into any agent the Skills CLI supports; then ask the agent to “security audit this codebase”:

npx skills add https://github.com/cloudflare/security-audit-skill --skill security-audit

Read any third-party skill before installing it; security review skills for AI coding agents covers vetting. Agent reviews complement the scanners and never replace them: Anthropic’s Claude Security docs say two scans of the same code can surface different findings, and that the plugin does not replace static analysis or dependency scanning.

Then read the risky code yourself

Read these line by line: session and auth middleware, every route that takes an ID, the payment webhook (signature verified, amount taken from the server, not the request), password reset, file upload and admin routes.

Environments and secrets

  • Three environments. Development, staging and production each get their own database and API keys; only production holds live Stripe keys.
  • Secrets live in the platform’s secret store or server environment variables. Keep .env files out of git; Next.js’s starter template already ignores them.
  • Rotate any key that was ever committed or pasted into a chat. GitHub’s own guidance makes revoking or rotating the first step, because clones and forks keep copies even after history is rewritten.
  • Least privilege. The app’s database user should not own the schema or be a superuser; migrations run as a separate user. Give each environment its own AI provider key and spend limit.

Choosing where to deploy

Checked on 11 November 2026, on each vendor’s own page.

Option Good fit Free tier notes
Vercel Next.js and front ends with functions Hobby is free but limited to non-commercial, personal use; spend management is a Pro feature
Netlify Static and front-end apps with functions Free plan lists a 300 credit limit
Cloudflare Workers and Pages Static sites and APIs at the edge 100,000 Worker requests a day, 10 ms CPU per invocation; static asset requests free and unlimited
Render Web services, workers, Postgres Free web services spin down after 15 idle minutes; free Postgres expires after 30 days; Render says not for production
Fly.io Containers close to users No free tier for new organisations; a trial with $1 of credit for 7 days
Railway Repo-to-service deploys with databases One-time $5 trial credit; Hobby is $5 a month including $5 of usage
Your own VPS Long-running workers, websockets, fixed monthly cost You own patching, firewall, TLS, backups and monitoring

With no one on call for servers, choose a managed platform; choose a VPS for processes a platform will not run, or a predictable bill at steady load. Hosting a site free on Cloudflare Workers walks through the cheapest managed path.

If you run your own server

The baseline, in order:

  1. A non-root user with sudo; SSH keys only, root login and passwords off.
  2. A firewall that denies inbound by default and allows only 22, 80 and 443 (or only Cloudflare’s ranges, below).
  3. Automatic security updates, and fail2ban or CrowdSec on SSH.
  4. The app runs as its own unprivileged user under systemd, listening on 127.0.0.1, behind nginx or Caddy.
  5. The database listens on localhost or a private network, never the public internet.
  6. Backups copied off the server, restored once to prove they work.
# /etc/systemd/system/my-app.service
[Unit]
Description=my-app
After=network.target

[Service]
User=myapp
WorkingDirectory=/srv/my-app
EnvironmentFile=/etc/my-app/env
ExecStart=/usr/bin/node server.js
Restart=on-failure
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

The Linux server hardening checklist has the exact commands for each step on Ubuntu 24.04, including how to test SSH changes before you log out.

Put Cloudflare in front

  1. Move DNS to Cloudflare and set web records to Proxied (orange cloud), which hides your origin. DNS-only records expose it and get none of Cloudflare’s security features.
  2. SSL/TLS: Full (strict). It validates the origin certificate, which must be publicly trusted or from Cloudflare’s Origin CA.
  3. Always Use HTTPS on. Cloudflare advises against also redirecting at the origin, which can loop.
  4. HSTS under Edge Certificates, once every subdomain serves HTTPS, without preload at first. Afterwards, do not pause Cloudflare, switch records to DNS-only or turn SSL off: if HTTPS breaks, visitors are locked out until the max-age runs out.
  5. WAF: turn on the Cloudflare Free Managed Ruleset (available on all plans) and use your one Free-plan rate limiting rule on login. It counts by IP over 10 seconds and matches only path and verified-bot status: (http.request.uri.path eq "/api/auth/login").
  6. Bot Fight Mode cannot be skipped by WAF custom rules, “may challenge API or mobile app traffic”, and needs CSP changes for its JavaScript detections. Leave it off for apps that serve an API or a mobile client.
  7. Lock the origin. Allow 443 only from Cloudflare’s ranges, or use Cloudflare Tunnel, where cloudflared connects outbound only and the origin needs no public IP. Mail on the web server and DNS-only records both leak the origin IP.
  8. Turnstile on sign-up and password reset: free for up to 20 widgets with unlimited challenges. Validate every token server-side with Siteverify; tokens last 300 seconds and are single use.
for cidr in $(curl -fsS https://www.cloudflare.com/ips-v4) $(curl -fsS https://www.cloudflare.com/ips-v6); do
  sudo ufw allow proto tcp from "$cidr" to any port 443 comment 'Cloudflare'
done
sudo ufw delete allow 80/tcp
sudo ufw delete allow 443/tcp

Plan-by-plan detail is in Cloudflare security and performance settings by plan.

Security headers and cookies

Set headers once, at Cloudflare or the origin. Values follow the OWASP HTTP Headers Cheat Sheet, except HSTS: OWASP suggests two years with preload, and one year without preload is a safer start. Drop that line if Cloudflare already sends HSTS.

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;

frame-ancestors 'none' stops other sites framing your app. A real app’s CSP needs more sources than this, so ship it first as Content-Security-Policy-Report-Only, which reports violations without enforcing, then switch. OWASP also recommends removing X-Powered-By and making Server uninformative.

Session cookies:

Set-Cookie: __Host-session=YOUR_SESSION_ID; Path=/; Secure; HttpOnly; SameSite=Lax

Secure sends it only over HTTPS, HttpOnly hides it from JavaScript, and SameSite=Lax keeps it off cross-site POST requests. The __Host- prefix makes the browser reject the cookie unless it has Secure, Path=/ and no Domain.

After deploy: test from outside

DOMAIN=app.example.com

# HTTP redirects to HTTPS; security headers present; no version banners
curl -sI "http://$DOMAIN" | head -n 3
curl -sI "https://$DOMAIN" | grep -iE 'strict-transport|content-security|x-content-type|referrer-policy|set-cookie|server|x-powered-by'

# Files that must never be served: expect 404 or 403
for p in .env .git/config; do curl -s -o /dev/null -w "%{http_code} /$p\n" "https://$DOMAIN/$p"; done

# Error pages: no stack traces, file paths or framework banners
curl -s "https://$DOMAIN/this-page-does-not-exist" | head -n 20
curl -s -X POST "https://$DOMAIN/api/YOUR_ROUTE" -H 'Content-Type: application/json' -d '{bad json' | head -n 20

# If the site is public
curl -s "https://$DOMAIN/robots.txt"

Confirm no server secret reached the client bundle. Run this in bash after a production build, with the production variables loaded:

gitleaks dir -v .next/static   # or dist/ for Vite
for name in DATABASE_URL STRIPE_SECRET_KEY OPENAI_API_KEY SUPABASE_SECRET_KEY; do
  value="${!name:-}"
  [ -n "$value" ] && grep -rqF -- "$value" .next/static dist 2>/dev/null && echo "LEAK: $name is in the client bundle"
done

Then the external tests:

  • SSL Labs Server Test for the TLS setup; tick “Do not show the results on the boards”.
  • Mozilla HTTP Observatory on MDN for headers and related settings.
  • PageSpeed Insights on the live URL: lab data from Lighthouse, plus field data from the Chrome UX Report once you have traffic.
  • ZAP baseline scan, only against your own app. It spiders for a minute and scans passively, without attacks:
docker run -v "$(pwd)":/zap/wrk/:rw -t ghcr.io/zaproxy/zaproxy:stable \
  zap-baseline.py -t "https://$DOMAIN" -r zap-report.html

Monitoring and alerts

Check from outside, on the home page, the login page and a /health route that touches the database. Free tiers, checked on 11 November 2026:

Service Free tier
UptimeRobot 50 monitors at a 5-minute interval, personal and commercial use; SMS and voice credits not included
Better Stack 10 monitors and heartbeats, 1 status page, Slack and email alerts; aimed at personal projects
Cloudflare Health Checks Not on Free; 10 on Pro, 50 on Business
New Relic 500 synthetic checks a month; simple ping monitors do not count
Sentry (Developer) 1 user, 5k errors, 1 uptime monitor, 1 cron monitor

Send the critical alert somewhere that wakes you, not only email. Add error tracking with personal data scrubbed, and ship app and proxy logs off the server; the New Relic free tier guide sets up checks and alerts end to end. Then add:

  • Certificate expiry for the origin certificate and any hostname not behind Cloudflare.
  • Domain expiry: auto-renew on, with a card that will still be valid.
  • Billing: the AI provider limits above, plus a cloud budget. AWS Budgets alerts on actual and forecast spend by email or SNS, though its data can lag by several hours. The AWS cost optimisation checklist covers the rest.

Backups and a simple incident plan

pg_dump --format=custom --file="backup-$(date +%F).dump" "$DATABASE_URL"
createdb restore_test
pg_restore --dbname=restore_test --no-owner "backup-$(date +%F).dump"
  • Backups run automatically, sit outside the server and the hosting account, and are restored into a scratch database monthly.
  • Paging: one named person and one backup, with phone numbers in the alerting tool.
  • Rollback: know the button or command that redeploys the previous version, and use it once before launch.
  • Key rotation: one list of every key, where it is used and how to rotate it.
  • Compromise: if a server may be breached, follow the compromised Linux server first-hour runbook before touching anything.

Launch checklist

Before launch

  • No secrets in git history or in NEXT_PUBLIC_ and VITE_ variables
  • RLS on every exposed Supabase table, or Firebase rules scoped to owners; buckets private
  • Ownership checked on every route, tested with two accounts
  • Rate limits on auth, AI and payment routes; hard spend limits at each AI provider
  • CORS allow-list, debug off, demo accounts and default passwords gone
  • Every new package confirmed real; lockfile committed
  • Secret, dependency and SAST scans clean
  • Agent review run; auth and payment code read by a person
  • Playwright smoke tests pass on staging

Deploy

  • Separate dev, staging and production keys
  • Every key ever committed or pasted into a chat rotated
  • Server: non-root, keys only, firewall, auto-updates, app unprivileged, database private
  • Cloudflare: proxied, Full (strict), Always Use HTTPS, HSTS, WAF, rate limit, origin locked
  • Turnstile validated server-side
  • Security headers and Secure; HttpOnly; SameSite session cookies

After launch

  • curl checks pass: redirect, headers, no .env, clean error pages
  • No server secret in the built bundle
  • SSL Labs, HTTP Observatory, PageSpeed Insights and ZAP baseline run
  • Smoke test passes on production

Ongoing

  • Uptime, error, certificate, domain and billing alerts reach a phone
  • Backups restored monthly; scanners in CI on every pull request
  • Rollback and key rotation tried once

Sources

  1. OWASP Top 10:2025 top10.owasp.org
  2. A01:2025 Broken Access Control (OWASP Top 10) top10.owasp.org
  3. A02:2025 Security Misconfiguration (OWASP Top 10) top10.owasp.org
  4. OWASP API Security Top 10 2023 api-security.owasp.org
  5. HTTP Security Response Headers Cheat Sheet (OWASP) cheatsheetseries.owasp.org
  6. How to use environment variables in Next.js (Next.js docs) nextjs.org
  7. Env variables and modes (Vite docs) vite.dev
  8. Row Level Security (Supabase docs) supabase.com
  9. Understanding API keys (Supabase docs) supabase.com
  10. Database advisors (Supabase docs) supabase.com
  11. Storage buckets (Supabase docs) supabase.com
  12. Avoid insecure rules (Firebase Security Rules docs) firebase.google.com
  13. Blocking public access to your Amazon S3 storage (AWS docs) docs.aws.amazon.com
  14. Access-Control-Allow-Origin (MDN) developer.mozilla.org
  15. Debugging application errors (Flask docs) flask.palletsprojects.com
  16. Django settings: DEBUG (Django docs) docs.djangoproject.com
  17. We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs (arXiv, USENIX Security 2025) arxiv.org
  18. Spend limits (OpenAI API docs) developers.openai.com
  19. Workspaces (Claude Platform docs) platform.claude.com
  20. Commands (Claude Code docs) code.claude.com
  21. Catch security issues as Claude writes code (Claude Code docs) code.claude.com
  22. Scan your codebase for vulnerabilities (Claude Code docs) code.claude.com
  23. trailofbits/skills (GitHub) github.com
  24. cloudflare/security-audit-skill (GitHub) github.com
  25. Developer commands (OpenAI Codex docs) learn.chatgpt.com
  26. gitleaks/gitleaks (GitHub) github.com
  27. trufflesecurity/trufflehog (GitHub) github.com
  28. google/osv-scanner (GitHub) github.com
  29. npm audit (npm docs) docs.npmjs.com
  30. CLI reference (Semgrep docs) docs.semgrep.dev
  31. Configuring default setup for code scanning (GitHub docs) docs.github.com
  32. GoogleChrome/lighthouse (GitHub) github.com
  33. About PageSpeed Insights (Google for Developers) developers.google.com
  34. Installation (Playwright docs) playwright.dev
  35. Testing (Stripe docs) docs.stripe.com
  36. Removing sensitive data from a repository (GitHub docs) docs.github.com
  37. Vercel Hobby plan (Vercel docs) vercel.com
  38. Netlify pricing netlify.com
  39. Pricing (Cloudflare Workers docs) developers.cloudflare.com
  40. Deploy for free (Render docs) render.com
  41. Pricing (Fly.io docs) docs.fly.io
  42. Plans (Railway docs) docs.railway.com
  43. Proxy status (Cloudflare DNS docs) developers.cloudflare.com
  44. Full (strict) encryption mode (Cloudflare SSL/TLS docs) developers.cloudflare.com
  45. Always Use HTTPS (Cloudflare SSL/TLS docs) developers.cloudflare.com
  46. HTTP Strict Transport Security (Cloudflare SSL/TLS docs) developers.cloudflare.com
  47. Managed Rules (Cloudflare WAF docs) developers.cloudflare.com
  48. Rate limiting rules (Cloudflare WAF docs) developers.cloudflare.com
  49. Bot Fight Mode (Cloudflare bot solutions docs) developers.cloudflare.com
  50. Protect your origin server (Cloudflare Fundamentals docs) developers.cloudflare.com
  51. Cloudflare Tunnel (Cloudflare One docs) developers.cloudflare.com
  52. Validate the token (Cloudflare Turnstile docs) developers.cloudflare.com
  53. Turnstile plans (Cloudflare Turnstile docs) developers.cloudflare.com
  54. Set-Cookie (MDN) developer.mozilla.org
  55. Content-Security-Policy-Report-Only (MDN) developer.mozilla.org
  56. HTTP Observatory (MDN) developer.mozilla.org
  57. SSL Server Test (Qualys SSL Labs) ssllabs.com
  58. ZAP baseline scan (ZAP docs) zaproxy.org
  59. UptimeRobot pricing uptimerobot.com
  60. Better Stack pricing betterstack.com
  61. Health Checks (Cloudflare docs) developers.cloudflare.com
  62. Synthetics usage limits (New Relic docs) docs.newrelic.com
  63. Sentry pricing sentry.io
  64. Managing your costs with AWS Budgets (AWS docs) docs.aws.amazon.com