// docs / security guides / pre-ship checklist
The vibe coding security checklist: 51 items before you ship
A practical, phase-organised checklist for apps built with Cursor, Claude Code, Lovable, Bolt, v0, Replit, and Windsurf. Each item is actionable in under five minutes. Run through it before you push to production, then again before each major release. Items are grouped into seven categories β secrets, database, auth, headers, third-party, deployment, monitoring β and tagged with the deploy phase they apply to.
PRE = pre-deploy (audit your source). DEPLOY = at deploy time. POST = post-deploy verification.
Secrets and API keys (8 items)
Hardcoded keys are the single most common finding in vibe-coded apps. Eight items to keep them out:
- PRE β Audit
NEXT_PUBLIC_env vars. Anything prefixedNEXT_PUBLIC_ships in client bundles. If one is a Supabaseservice_rolekey (decodes to JWT with"role":"service_role"), delete it and route through a server-only client (src/lib/supabase/service.tswithimport 'server-only'). - PRE β Grep for hardcoded provider keys. Search source for
sk_live_,pk_live_,STRIPE_SECRET,sk-ant-,sk-,AIza,AKIA, and JWT-looking strings (eyJ). Move every hit into.env.localand reference viaprocess.env.*server-side only. - PRE β Verify
.gitignore. Confirm.env*.local,.npmrc,.yarnrc, and any provider-specific credentials files are ignored. Rungit ls-filespiped through your provider patterns to find anything already committed. - PRE β Scan the built bundle. Run
npm run build, then grep.next/staticand anydist/output for the same patterns. If a key reaches the bundle, the dev never had clean env separation. - DEPLOY β Set secrets per environment. Vercel: Settings β Environment Variables, scope each to Production / Preview / Development. Never share
sk_live_*with the Preview env. Use Vercel's encrypted env-var storage, not inline workflow secrets. - DEPLOY β Disable build-log secret echo. Some CI configs
echoenv vars during build. Audit yourvercel.json, GitHub Actions workflows, or Cloudflare Pages settings for anyecho $SECRETthat would push the value into public build logs. - POST β Run a passive scan. FixVibe's Free tier covers this: paste the deployed URL, wait ~20s, look for
secrets.*findings. The Secrets in JavaScript Bundles check catches keys that landed inlocalStorageorsessionStoragevia a misuse of the SDK. - POST β Rotate any key that ever shipped. If a key was in a public bundle for even minutes, treat it as compromised. Rotate Supabase service-role keys via the dashboard, regenerate Stripe restricted keys, revoke Anthropic / OpenAI / Google keys via their consoles.
Database access control: RLS and Firestore rules (6 items)
BaaS defaults are permissive on purpose so the first tutorial works. Production needs explicit policies.
- PRE β Enable RLS on every
public.*table. In Supabase: each table needsALTER TABLE ... ENABLE ROW LEVEL SECURITYplus explicit policies for each role. RLS is what stops theanonandauthenticatedAPI roles;FORCE ROW LEVEL SECURITYonly extends it to the table owner and is optional defence in depth. - PRE β Write a policy per (table, role, action). Minimum: a SELECT policy that joins on
auth.uid(). Better: separate INSERT / UPDATE / DELETE policies so an UPDATE can't smuggle inuser_idchanges that reroute ownership. - PRE β Replace default Firebase rules. Default test-mode rules read
allow read, write: if true;. Replace with auth-bound rules per collection:match /users/{userId}withallow read, write: if request.auth.uid == userId; - PRE β Lint migrations in CI. Run
supabase db lintor an equivalent before merging. CI should fail the build if anyCREATE TABLE public.*lacks a matching RLS policy. - DEPLOY β Confirm RLS survived deploy. Re-check in Supabase Studio after deploy: Tables β each row β RLS toggle is ON. Production database migrations occasionally race ahead of policy files; verify the policy is live.
- POST β Run an active scan against a verified domain. The Supabase Row-Level Security active check writes to a tiny seed row using the anon key and reports back if the write succeeded β i.e. RLS isn't actually enforcing.
Authentication and sessions (7 items)
Auth bugs in AI-coded apps tend to be subtle: an off-by-one in token verification, a missed HttpOnly flag, a getSession() where there should be a getUser().
- PRE β Replace
getSession()withgetUser().getSession()reads the cookie and trusts it;getUser()verifies with the Supabase backend. On server routes always usegetUser(). - PRE β Confirm token expiry. Magic-link, password-reset, and email-verification tokens need server-enforced expiry. Default Supabase magic-links expire after 1 hour β don't override that to a higher number without a real reason.
- PRE β Verify JWT
audandexp. If you decode tokens manually anywhere, check both claims. Better: use the SDK'sgetUser()which does it for you. - PRE β Audit cookie flags. Custom session cookies should be
Secure; HttpOnly; SameSite=Lax(orStrictfor non-OAuth flows). No session material inlocalStorage. - PRE β Validate the
nextredirect param. Thenextquery param after sign-in must start with/and not//(open-redirect to attacker.example). Reject anything else server-side. - POST β Test logout. Sign in, sign out, inspect cookies (DevTools β Application β Cookies). The session cookie must be cleared on the same response. If it persists, the logout handler isn't actually destroying server-side state.
- POST β Active probe. The Auth Flow Defects and Account Enumeration checks surface broken auth boundaries β different responses on "user exists" vs "wrong password", missing rate-limit on login, unsigned reset tokens.
HTTP security headers and Content Security Policy (6 items)
Headers are the cheapest hardening in the entire pipeline and the most consistently skipped by codegen.
- PRE β Ship a real CSP. Minimum:
script-src 'nonce-{NONCE}' 'strict-dynamic'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'. No'unsafe-inline'inscript-src. Next.js auto-applies the nonce when middleware sets thex-noncerequest header. - PRE β Add the legacy three.
X-Content-Type-Options: nosniff,X-Frame-Options: DENY(or rely on CSPframe-ancestorsalone),Strict-Transport-Security: max-age=31536000; includeSubDomains. - PRE β Tighten
Referrer-Policy. Defaultstrict-origin-when-cross-originis fine for most apps. Don't shipunsafe-urlor no header at all. - PRE β Replace
Access-Control-Allow-Origin: *. Grep for it. Replace with an explicit origin allowlist. Anywhere it's*alongsidecredentials: include, the browser will refuse the request β but that's no defence against a misconfigured backend. - DEPLOY β Verify headers post-deploy. Open DevTools β Network β click your root document β Headers tab. CSP, HSTS, X-Frame-Options, X-Content-Type-Options should be present. CSP must not have
'unsafe-inline'inscript-src. - POST β Run HTTP Security Headers. The passive header check reports each missing header with deploy-platform fix guidance (Vercel
vercel.json, Cloudflare Pages_headers, Netlify_headers, Next.js middleware).
Third-party integrations and APIs (5 items)
Every script you include is a CSP exemption and a potential supply-chain surface. Treat third parties as part of your trust boundary.
- PRE β Reverse-proxy analytics where possible. PostHog, Plausible, Umami all support proxying through your own domain (e.g.
/api/posthog). This keepsconnect-srcon the same origin and survives ad-blockers. - PRE β CSP-allowlist the rest. For Google Analytics, Stripe.js, Sentry, Intercom, GTM, etc., add each vendor's origins to the matching CSP directive (
script-srcfor loaders,connect-srcfor telemetry,frame-srcfor iframes,img-srcfor pixels). - PRE β Use Stripe Checkout, not raw card forms. Stripe Checkout is a top-level redirect; no CSP entry needed for the script. Hosted PCI surface lives entirely on Stripe's domain. Roll your own only if you have a strong reason.
- PRE β Lock
package-lock.jsonin CI. Runnpm ci(notnpm install) in production builds. Audit dependencies withnpm auditor Snyk before each release. - POST β Watch Technology Fingerprinting. The passive tech-stack discovery surfaces library versions visible to a crawler. If you ship an EOL React, jQuery, or Bootstrap, FixVibe flags it and links to known CVEs.
Deployment hygiene and infrastructure (8 items)
How you deploy matters as much as what you deploy. AI-coded apps especially benefit from explicit deploy hardening.
- PRE β Disable
x-powered-by. Innext.config.js:poweredByHeader: false. Removes a free version-disclosure signal. - PRE β Confirm middleware lives at
src/middleware.ts. With thesrc/directory layout, Next.js ignores a root-levelmiddleware.ts. Misplaced middleware silently fails to set CSP / auth headers / rate limits. - PRE β Sanity-check Vercel deployment protection. Production should be public; Preview should be password-protected or limited to org members. Vercel-Specific Exposure reports the surface.
- PRE β Block dotfile and config probes at the edge. Add a rewrite or a deny rule for
/.env,/.git/*,/.aws/*,/.next/tracepatterns. Vercel returns 403 for many of these by default; cross-check. - DEPLOY β Separate environments. Production, Preview, Development. Each gets its own set of secrets. Live keys never reach Preview, Stripe test mode never reaches Production.
- DEPLOY β Enable Vercel Web Application Firewall. Pro and Enterprise plans include WAF with managed rules. Cloudflare Pages has Bot Fight Mode. Both reduce automated-scanner abuse and password-spray load.
- POST β Verify TLS configuration. SSL Labs or testssl.sh against your production domain. TLS 1.2 minimum, prefer TLS 1.3, no weak ciphers, HSTS preload eligible.
- POST β Confirm health-check endpoints are minimal. A
/api/healthshould return200 OKwith no body. Don't echo environment, build hash, or deploy timestamp without auth.
Ongoing monitoring and re-scanning (4 items)
Security is not a one-shot pre-ship audit. Drift happens on every deploy.
- Verify your production domain in FixVibe. Dashboard β Domains β DNS TXT or HTTP file verification. Active scans require Hobby or above, scheduled scans require Pro or Unlimited, and live threat monitoring requires Unlimited.
- Schedule passive re-scans on Pro or Unlimited. Choose an available cadence for your verified domain. Scheduled runs share your scan allowance; enable completion notifications and signed webhooks as needed. A scheduled scan observes the app at run time, not continuously.
- Wire outbound webhooks. Account β Webhooks β add an HTTPS endpoint, subscribe to
scan.completed+finding.created+scan.active_api.first_used. Route into Slack / Discord / PagerDuty. - Enable live threat monitoring on Unlimited. Periodic certificate, DNS, bundle, and threat-intelligence checks can surface changes between scheduled scans. Coverage and alert timing depend on the signal and successful polling.
Replit Agent
If you build with Replit Agent, also check:
- Don't paste keys into the Agent prompt. Add them in Secrets and give the Agent the variable name instead, so the value never appears in generated code or chat history.
- Test CORS on the production domain. The preview pane and your deployed URL are different origins. Check your CORS allowlist against the domain users actually load.
- Replit Agent sometimes hardcodes test values. The Agent might generate code with
API_URL = 'http://localhost:5000</code>. Before shipping, replace with <code>os.environ[API_URL'].
Firebase Studio
If you build with Firebase Studio, also check:
- Test-mode rules are temporary by intent, not by default. Test mode lets anyone read and overwrite data. If the generated rule carries a date cutoff, client access stops on that date; if not, the database stays open. Replace test rules before launch either way.
- Security rules protect data, not Hosting. Firestore and Storage rules don't govern what Firebase Hosting serves: everything in the public directory is public. Keep exports, backups and admin pages out of it.
Claude Code
If you build with Claude Code, also check:
- PRE β Keep personal Claude Code settings out of git. Commit
.claude/settings.jsonfor shared project settings, but keep.claude/settings.local.jsonout of the repo: Claude Code excludes it from git when it creates the file, so check it if you created it by hand. Session history lives in~/.claude, outside the project (Claude Code docs). - Bash operations are unverified. Claude Code executes bash commands directly.
git commit -m "fix"is convenient, but if the working directory has a.envfile and a glob includes it, it commits. Always reviewgit diff --cachedbefore Claude Code commits.
Next steps
Want the educational backdrop on why these items matter? Read AI-generated code security scanning. Want concrete code snippets for each hardening step? See How to secure an app built with AI coding tools.
