The security posture of vibe-coded apps
What AI-assisted development gets right and wrong about secrets — the systematic failure pattern, and a hardening sequence that fits how these apps ship.
The security posture of vibe-coded apps
For people building software primarily by directing AI assistants: the specific ways this workflow handles secrets well, the specific way it fails, and a hardening sequence matched to how these apps actually ship.
"Vibe coding" — building applications by conversing with AI tools rather than writing most code directly — has produced a wave of deployed software built by people who never chose a security posture deliberately. That phrase sounds like an accusation, so let's start by discarding the moral framing: these developers aren't negligent. They're using a workflow whose mechanics interact with secrets in predictable ways, producing predictable outcomes. Some of those outcomes are better than traditional development manages. One is worse, and it's structural.
This piece maps the mechanics honestly: what AI-assisted development systematically gets right, the single failure pattern that dominates incidents, why the failure is architectural rather than careless, and a hardening sequence that works within the workflow instead of demanding it stop. The companion pieces on where keys migrate and framework prefix conventions cover the underlying machinery in depth; this article stays at the posture level. Guidance for teams adopting these workflows deliberately also lives at the /for section.
What the workflow gets right by default
Credit where due — several practices traditional teams struggle to enforce happen automatically in AI-assisted development:
Secrets start outside source code. Assistants scaffold .env files and reference environment variables because training corpora overwhelmingly show that pattern. The median generated codebase begins life more git-hygienic than the median human-started one: .gitignore includes .env from template one, keys live in env-var references from the first commit.
Quickstart compliance. Generated integrations follow provider documentation closely — Stripe calls go through server-side SDK initialization, Supabase clients instantiate with the documented key arguments. The code is usually idiomatic for the library it invokes.
Consistency. An assistant applies the same configuration approach everywhere, eliminating the hand-rolled variance — five different config-loading schemes — that plagues organically grown codebases.
These are real advantages. Dismissing vibe-coded apps as uniformly insecure misreads the evidence; their baseline habits beat plenty of professional codebases. The problem isn't the baseline. It's what happens next.
The systematic failure: the key goes where the code runs
One failure pattern dominates, and it follows mechanically from how the tools work:
The assistant places code where it executes, and secrets travel with it. When you ask for "a feature that calls OpenAI," the assistant writes that call into the file you're working in — often a component destined for the browser. The request specified what, not where; the agent optimizes for the code running correctly in the context it sees. In frontend-heavy stacks, that context is the browser.
Then the friction appears: the build can't resolve process.env.OPENAI_API_KEY in client code. The fastest repair path — rename the variable with the framework's public prefix, or paste the value literally — compiles, runs, and demos perfectly. The diff looks trivial: one variable name changed. Nobody reviewing it needs to know what substitution that prefix performs. The feature ships, working exactly as requested, with the key now serialized into every visitor's download.
This loop deserves naming precisely because it involves no mistake anyone made: not the developer, who asked for something reasonable; not the assistant, which wrote correct code for its context; not the bundler, which did exactly what its configuration promises. Every step was locally correct. The vulnerability emerges from composition — and composition is invisible to people reviewing diffs rather than architecture.
Stack mechanics amplify the pattern predictably:
| Environment | Amplifier | Mechanism |
|---|---|---|
| Browser-only builders (Vite SPAs) | No server exists | The only place a key can go is the bundle; prefixes like VITE_ inline values at build time |
| Agent editors (Cursor-class) | Imitation | Once one module reads import.meta.env, subsequent features follow the established pattern — leaks spread by precedent, not by repeated mistakes |
| Build-time injection platforms | Prefix temptation | Platform-injected secrets become visible to frontend code the moment someone adds a client-facing prefix |
| Full-stack frameworks | False confidence | Server components handle keys safely — until one refactor moves the call client-side, where the same line silently becomes a leak |
The last row cuts against intuition worth stating plainly: full-stack frameworks are safer here not because they prevent the mistake but because they make the safe path available in the same file structure, so the drift distance is shorter.
The missing safety net: no one is watching
Traditional teams fail through oversight gaps too, but they possess compensating structures vibe-coded projects lack entirely:
- Review asymmetry: solo builders review their own diffs, and AI-generated diffs are large, plausible-looking, and tedious to audit line-by-line. The one-line prefix change hides inside a hundred-line feature commit.
- No institutional memory of incidents: teams remember last year's leak; individuals building alone have only their own. Rotation discipline and incident response are learned culturally, and culture requires multiple people.
- No separation of duties: the person writing code approves it, deploys it, and holds every credential it uses. Speed is the entire point of the workflow — gates feel like friction because, for one person, they genuinely are.
- Provider caps unset: dashboards full of defaults mean no budget limits, no hard usage ceilings, no IP allowlists. The controls exist; configuring them is exactly the unglamorous task the workflow skips.
None of this argues against the workflow. It argues that its security posture must be mechanical — embedded in defaults and automation rather than dependent on judgment calls, because judgment is precisely what's being outsourced.
A hardening sequence that fits the workflow
Ordered by impact-per-minute-spent, designed to survive being executed once and mostly forgotten:
- Inventory every key in play (30 minutes). List providers, key types, and where each value currently lives:
.env, platform settings, pasted literals. The free scan answers the deployment half in minutes; the platform dashboards answer the rest. - Move privileged calls behind your server (an afternoon). Every secret-tier key — Stripe
sk_live_, Supabasesb_secret_, LLM keys, AWS pairs — belongs in a backend route the client calls. Full-stack frameworks make this a small file; browser-only stacks need the smallest possible edge/serverless function. This single step converts the dominant failure pattern into a non-issue. - Cap the blast radius (20 minutes per provider). Budget/hard limits on LLM projects, RAKs instead of Stripe secret keys, restricted Supabase policies tested with real anon-role queries (the Supabase test procedure). If a key leaks tomorrow, these settings decide whether it's a bad afternoon or a bad month.
- Scan what ships, on every ship (one-time setup). Point artifact scanning at the deployed URL and wire it to re-run per deploy — the mechanical replacement for the review layer that solo workflows lack. Monitoring exists precisely because manual checks decay within weeks.
- Enable push protection and a pre-commit scanner (15 minutes). Catches the accidental-commit path that remains even with good intentions.
- Rotate on a schedule (quarterly, 10 minutes each). Calendar entry, not intention. The rotation playbook makes each instance boring.
Steps 1–3 bound damage; steps 4–6 catch recurrence. Together they constitute a posture — chosen deliberately, in about one working day — that matches what the workflow actually needs.
Provider caps: the twenty-minute sweep
Hardening step three deserves expansion because "cap the blast radius" compresses twenty minutes of dashboard work that most builders have never done once. Per provider, the high-yield moves:
- LLM platforms: set the monthly budget and hard usage limit on every project — the hard limit actually stops traffic at the ceiling rather than merely alarming. OpenAI projects expose both (production guidance); Anthropic workspaces take spend limits. One capped agent-project per tool keeps an assistant's runaway loop from touching production quotas.
- Stripe: replace
sk_live_usage with restricted keys scoped to what checkout needs (typically PaymentIntents write, nothing else), then attach an access policy allowing your server IPs/ASN — unauthorized use becomes blocked-and-reported instead of silently successful. - Supabase: disable the legacy
anon/service_roleJWTs once migrated (Settings → API Keys), leaving modernsb_publishable_/sb_secret_pairs only — halving credential inventory in one click. Then run the anon-role probe queries from the Supabase deep-dive to confirm policies fire. - AWS: create a billing budget alarm at an amount that would surprise you; if no IAM-user keys remain after federation cleanup, confirm none are marked active in the credential report.
- Everything else: walk provider dashboards once asking a single question — "if this key leaked tonight, what stops tomorrow being expensive?" Any provider without an answer gets one (spend caps, IP allowlists, scoped tokens) before integration continues.
The sweep's power is asymmetry: these settings cost minutes and change worst-case outcomes categorically. They also compose with everything else in the sequence — caps don't prevent leaks, but they convert hypothetical incidents into bounded ones, which is precisely what solo builders need given they lack the team capacity traditional response assumes.
Signals it's time to grow process beyond one person
Solo posture isn't a permanent state, and the transitions deserve named triggers rather than drift. Process overhead — reviews, rotation calendars, monitoring ownership — becomes worth its friction when any of these appear:
- A second person touches production. Shared credentials between two people means rotation now coordinates calendars, and "who changed this?" questions need answers the old setup never recorded.
- Revenue flows through the system. Payment processing, customer data, or SLAs convert downtime and disclosure from learning experiences into obligations.
- An enterprise customer asks about security posture. Questionnaires ask about scanning cadence, rotation policy, incident history — answers assembled post-hoc under deadline are worse than practices adopted early and boring.
- The first incident happens. Incidents reveal whether documentation, inventory, and response instincts exist; if the answer is "improvised successfully," that's luck scheduling its exit.
Each trigger maps to specific additions rather than generic "get serious": trigger one adds shared secret storage and review habits; trigger two adds the caps sweep plus monitoring; trigger three adds written policy pages distilled from what you already do; trigger four adds the incident runbook rehearsed while calm. Nothing requires hiring security staff — the same mechanical posture scales by adding written artifacts to habits already in place.
The five-minute architecture check
Before any hardening work, map where calls actually run — the answer determines everything downstream. Walk each integration and answer one question: in which runtime does this code execute?
| Check | How to verify | If it fails |
|---|---|---|
| Provider calls live server-side? | Find the SDK/fetch call; trace its module's bundler target (client component markers, import.meta.env reads, 'use client' directives) | Move it behind a route handler or server function |
| No prefixed variable holds a secret-tier value? | Search env names against provider format tiers (the tier table) | Rename to server-only naming; relocate consumption |
| Browser receives only public-tier keys? | Inspect served chunks for credential formats (five-minute check) | Rotate first; then fix the path that shipped it |
| Database access bypasses the client? | Connection strings appear nowhere in client-reachable modules; data flows through your API | Introduce an API layer, however thin |
| Auth checks run where data lives? | Authorization decisions execute server-side per request, not in component logic | Re-home authorization into server queries/policies |
The check's power is its speed: fifteen minutes across a typical vibe-built app produces a named list of moves rather than a mood of unease. Each failed row maps directly onto a fix guide at /fix, most of which reduce to "move this call behind this route." Run it after major features, before launches, and whenever handing a project to someone else — the moments when architecture drift silently.
One habit multiplies the check's value: record answers as you go. A running architecture note ("Stripe = server route ✓, OpenAI = client ⚠ pending move") turns security from episodic worry into tracked state, which is precisely what solo builders lack and tooling can't replace.
What the hardened posture looks like six months in
Concrete end-state beats abstract principle, so here is the whole sequence as it settles into daily reality:
- Every secret-tier call runs server-side; client code knows publishable keys and API routes, nothing else.
- Provider caps configured everywhere and reviewed when integrations change — the twenty-minute sweep became a habit attached to feature launches.
- Monitoring watches every deploy, alerts route to one channel, and regressions escalate automatically (why regressions differ).
- Rotation happens quarterly by calendar, each instance boring, each one downscoping something (the playbook).
- The architecture note stays current — the five-minute check's answers recorded, revisited at handoffs.
Total standing maintenance: perhaps an hour per month, most of it reading dashboards that confirm silence. Compare that against the alternative baseline most vibe-built projects start from — uncapped LLM projects, legacy Supabase JWTs live, no artifact visibility — and the sequence's value shows as removed tail risk rather than added work. The workflow that built the app keeps running exactly as fast; the difference is that what ships gets checked by machinery instead of hope.
Why mechanical checks compound
Each item in the sequence is individually small, and their combination is where the posture actually lives — because mechanical checks share one property judgment-based security never has: they keep working while attention is elsewhere.
Consider the same refactor arriving twice. In a judgment-based setup, whether anyone catches the client-side key depends on who reviews, how tired they are, and how big the diff feels — variance that leaks exploit statistically rather than occasionally. In the mechanized setup, three independent layers react without being asked: the boundary guard fails the build on the server-name reference, the built-output scan flags whatever slipped past naming, and deploy-triggered monitoring catches anything the local build environment injected differently than CI's. The leak needs to beat three indifferent systems instead of one attentive human — and indifference scales; attention doesn't.
The compounding also runs in time: every catch feeds the rule set (tuning becomes gardening), every rotation updates inventory, every audit refreshes the architecture note. Six months of small mechanical checks produce a posture no single heroic review could, at a cost curve that flattens precisely where judgment-based effort steepens. For builders whose entire methodology is "make the machine do the repeated thing," this is the familiar pattern applied to a domain that marketing insists requires enterprise ceremony — it doesn't. It requires three scripts and a calendar entry, which the sequence above already names.
FAQ
Is vibe-coded software inherently less secure? Inherently is too strong. Its baseline habits are decent; its failure mode is concentrated and architectural rather than scattered and careless. With the hardening sequence applied, there's no reason these apps can't match conventional ones — and reasons to expect better consistency.
My assistant always puts keys in .env. Am I covered? Covered against git leaks, yes. But .env placement says nothing about which modules consume the values — the browser-bound component reading a prefixed variable pulls the value straight into the bundle regardless of its origin. Verify what ships, not what's stored.
Which single change matters most? Step 2. Moving secret-tier keys server-side eliminates the entire class of client-bundle leakage at the architectural level. Everything else detects or bounds what step 2 prevents.
Do I need enterprise tooling for any of this? No. Every step above uses free tiers, built-in platform features, or the free scan. Posture is about decisions and defaults, not procurement.
The workflow that builds your app can't audit what it deploys — that job stays yours, and it takes minutes. Run KeyDrift's free scan on your deployed app today, no signup, and see exactly what's shipping.