Bolt generates full-stack apps at speed. Here are the production gaps that appear most often in Bolt-built apps — and how to check yours.
Default stack: Supabase or SQLite · varies
Bolt's strength is that it will scaffold a whole stack — front end, API routes, database calls — from one prompt, and the result runs. The cost of that speed is that the boundary between what the browser can see and what only the server can see gets decided by a variable name, and variable names are exactly the kind of detail that gets generated rather than considered.
In a Vite app, any env var prefixed VITE_ is inlined into the client bundle at build time. In Next.js, NEXT_PUBLIC_ does the same thing. Neither framework warns you that the value you just exposed was a secret — that is not something a bundler can know. The result is a class of bug that is invisible in the running app and obvious in the network tab.
The other Bolt-shaped gaps are about what happens on input you did not expect: an API route that trusts its request body, a CORS config that trusts every origin, a component tree with nothing to catch a thrown error. Each of the checks below takes a minute or two.
The part that breaks first
Bolt's speed is the trap: it'll wire a working API route in seconds, secret key included as a VITE_ variable that ships straight to the browser. Your app works perfectly in the demo. It also hands your keys to anyone who opens the network tab.
And then this becomes your life
Now every deploy is a held breath. You re-read the env config before you push. You grep the bundle for keys you might have leaked. You tell yourself you'll 'do a proper security pass later' — and 'later' becomes a tab you never close.
Bolt promised you'd build the thing in a weekend and move on. Instead you're the one-person ops team for an app you don't fully understand, patching holes you didn't know you'd dug, on a codebase that moved faster than you could read it.
The good news: every one of these is findable, and most are fixable fast. Here's exactly what tends to be wrong in Bolt apps.
These aren't hypothetical — they show up in the free audits. See the full production-readiness guide →
| # | Gap | Severity | Where to look |
|---|---|---|---|
| 1 | Secrets exposed through client-side env prefixes | Critical | Build output / .env files |
| 2 | API routes that trust the request body | High | Server route handlers |
| 3 | CORS configured to allow any origin | High | API middleware / headers |
| 4 | String-interpolated SQL | Critical | Database access layer |
| 5 | No error boundaries in the component tree | Medium | Route-level components |
VITE_ and NEXT_PUBLIC_ are not naming conventions — they are instructions to the bundler to inline the value into JavaScript the browser downloads. A generated API route that needs a key gets one, and the fastest way to make it available is the prefix that always works. That prefix is also the one that publishes it.
How to check yours
List every prefixed variable, then decide for each one whether you would be comfortable printing it on the homepage. Anything that authenticates you to a paid service, a database, or a mail provider fails that test. Then confirm by searching the built bundle for the literal value.
# 1. Which variables are compiled into the client bundle?
grep -rhoE '(VITE_|NEXT_PUBLIC_)[A-Z0-9_]+' .env* src/ app/ 2>/dev/null | sort -u
# 2. Confirm a suspected secret actually shipped
npm run build
grep -r 'sk_live\|sk_test\|service_role\|SG\.' dist/ .next/ build/ 2>/dev/nullGenerated handlers commonly destructure straight off the parsed body and hand the values to a query. Missing fields become nulls, wrong types become runtime errors, and extra fields become whatever your ORM decides to do with them — including writing columns the user should never control, like role or is_admin.
How to check yours
Find every handler that reads the body without a schema parse. Then attack one: send a body with the wrong types and an extra privileged field. A validated route answers 400. An unvalidated one answers 500, or worse, 200.
# Handlers that read a body with no schema parse nearby
grep -rn 'await req.json()\|await request.json()\|req.body' app/ src/ pages/ 2>/dev/null
# Probe one of them
curl -i -X POST https://your-app.example/api/items \
-H 'Content-Type: application/json' \
-d '{"quantity":"not-a-number","role":"admin"}'A wildcard CORS policy lets any website on the internet call your API from a visitor's browser. On its own that is survivable; combined with cookie-based sessions and credentials allowed, it means any page your logged-in user visits can act as them against your API.
How to check yours
Send a request with a forged Origin header and read what comes back. An access-control-allow-origin of * is too broad. An echoed origin combined with access-control-allow-credentials: true is worse — that is an allow-list of everyone, with sessions attached.
curl -s -D - -o /dev/null \
-H 'Origin: https://not-your-site.example' \
https://your-app.example/api/items \
| grep -i 'access-control-'Template literals are the natural way to write a query when you are generating code, and they are also SQL injection. Parameterized queries and query-builder methods are safe because the driver sends the values separately from the statement; a template literal sends whatever the user typed as part of the statement.
How to check yours
Search for query calls whose argument is a template literal containing a variable. Every hit is either a bug or needs a comment explaining why the input cannot be user-controlled. Note that some libraries — Vercel Postgres and Neon's serverless driver among them — deliberately use a tagged template that parameterizes safely; confirm which one you have before rewriting anything.
grep -rnE '(query|execute|raw)\(\s*`[^`]*\$\{' src/ app/ lib/ 2>/dev/nullOne thrown error in one component unmounts the whole React tree. The user gets a blank white page with no explanation and no way back, and — because nothing was logged — you get no signal that it happened at all.
How to check yours
Check that every route has a boundary above it: error.tsx in a Next App Router segment, errorElement on a React Router route, or a class component with componentDidCatch. Then prove it works by throwing on purpose in a leaf component and confirming you get a fallback UI rather than a blank document.
# Next.js App Router
find app -name 'error.tsx' -o -name 'global-error.tsx'
# React Router
grep -rn 'errorElement' src/Work through these in order before you share the URL publicly. The items marked critical are the ones that expose data to people who never had an account.
No secret keys prefixed with VITE_ or NEXT_PUBLIC_critical
Built bundle searched for live keys and confirmed cleancritical
API route inputs validated with Zod or an equivalent schemacritical
CORS restricted to known origins, especially with credentials enabledcritical
Database queries use parameterized statements (no string interpolation)critical
Error boundaries at route level
The questions that come up most often about Bolt apps in production.
No. The VITE_ prefix tells Vite to inline that value into the JavaScript bundle it ships to the browser, so anyone can read it in devtools. The same is true of NEXT_PUBLIC_ in Next.js. Use those prefixes only for values you would be comfortable printing on your homepage — public URLs, feature flags, publishable keys — and keep everything else server-side.
Run your production build, then grep the output directory for the key's literal value or a recognizable prefix — sk_live and sk_test for Stripe, service_role for a Supabase service key, SG. for SendGrid. If it appears in dist/, .next/, or build/, it has been public for as long as that build has been deployed, so rotate the key rather than just moving it.
With access-control-allow-origin set to *, any website can call your API from a visitor's browser. That is mostly harmless for public read-only data, but if your API authenticates with cookies and you also allow credentials, any page your logged-in user visits can make authenticated requests as them. Restrict the allow-list to origins you control.
Generated handlers commonly read the request body directly and pass the values to a query, which means anything the client sends is trusted. Add a schema parse — Zod is the usual choice — as the first line of every handler, and return a 400 when it fails. Test it by sending a body with wrong types and an unexpected privileged field.
Error boundaries are for the paths you have not tested. Without one, a single unhandled exception unmounts the entire React tree and the user gets a blank page with no way forward, and nothing gets logged. One boundary per route turns a total outage into a recoverable error state you can actually see.
Get the free audit. Three quick fields, a written report personally reviewed by Shane Jordan — not a scanner. You'll know exactly what's wrong in your Bolt app and what to fix first.
Request Free AuditI take a limited number of audits at a time · priority review available