Platform Teardown · Bolt

Is your Bolt app actually safe for real users?

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.

Common gaps in Bolt apps

These aren't hypothetical — they show up in the free audits. See the full production-readiness guide →

#GapSeverityWhere to look
1Secrets exposed through client-side env prefixesCriticalBuild output / .env files
2API routes that trust the request bodyHighServer route handlers
3CORS configured to allow any originHighAPI middleware / headers
4String-interpolated SQLCriticalDatabase access layer
5No error boundaries in the component treeMediumRoute-level components

1.Secrets exposed through client-side env prefixes

Critical

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.

bash
# 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/null

2.API routes that trust the request body

High

Generated 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.

bash
# 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"}'

3.CORS configured to allow any origin

High

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.

bash
curl -s -D - -o /dev/null \
  -H 'Origin: https://not-your-site.example' \
  https://your-app.example/api/items \
  | grep -i 'access-control-'

4.String-interpolated SQL

Critical

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.

bash
grep -rnE '(query|execute|raw)\(\s*`[^`]*\$\{' src/ app/ lib/ 2>/dev/null

5.No error boundaries in the component tree

Medium

One 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.

bash
# Next.js App Router
find app -name 'error.tsx' -o -name 'global-error.tsx'
# React Router
grep -rn 'errorElement' src/

Bolt checklist

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.

  1. 1.

    No secret keys prefixed with VITE_ or NEXT_PUBLIC_critical

  2. 2.

    Built bundle searched for live keys and confirmed cleancritical

  3. 3.

    API route inputs validated with Zod or an equivalent schemacritical

  4. 4.

    CORS restricted to known origins, especially with credentials enabledcritical

  5. 5.

    Database queries use parameterized statements (no string interpolation)critical

  6. 6.

    Error boundaries at route level

Bolt production-readiness FAQ

The questions that come up most often about Bolt apps in production.

Are VITE_ environment variables safe to use for API keys?

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.

How do I check whether my Bolt app leaked a secret?

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.

What is the risk of a wildcard CORS policy?

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.

Does Bolt validate API inputs for me?

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.

Do I need error boundaries if the app works?

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.

Related

Find out before your users do.

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 Audit

I take a limited number of audits at a time · priority review available

Operational IntelligenceOur newsletter on operational AI, in your inbox.
Subscribe free →