Guide · Bolt
Bolt app security checklist
Bolt can scaffold a full-stack app in an afternoon. Before real users put real data into it, go through this list. Each item says what to look for, why it matters, and what to ask Bolt to change.
Bolt apps are usually a Vite + React frontend deployed as static files, with a database behind them (commonly Supabase). Everything in the frontend is downloaded by every visitor, so the core rule is simple: the browser is public; protection has to live in the database or on a server.
Secrets
1. No secret keys in the frontend
In a Vite app, any environment variable starting with VITE_ is copied into the JavaScript bundle at build time. That is correct for public values like the Supabase URL and anon key, and wrong for anything secret: Stripe secret keys, OpenAI or Anthropic keys, the Supabase service_role key, email API keys.
Check: open your live site, open the browser's developer tools, and search the loaded JavaScript for sk_live_, sk-, sb_secret_, re_, SG.. Fix: rotate the key at the provider first, then move the call into a server function. Details: which frontend keys are fine.
2. .env is not deployed or committed
Make sure .env is in .gitignore and that your host does not serve it. Try opening https://your-app/.env. Many hosts return the app's own index.html for unknown paths, so look at the content, not just whether the page loads.
Database
3. Row Level Security on every table
If you use Supabase, every table in the public schema needs RLS enabled and sensible policies. Without it, anyone with your public anon key can read and often modify the whole table. Use the queries in the Supabase RLS checker guide to list tables and policies.
Ask Bolt: "Enable Row Level Security on every table and add policies so users can only read, insert, update and delete rows where user_id = auth.uid(). Show me the SQL."
4. No policy that says using (true) on private data
A table with RLS turned on but a policy of using (true) is as open as a table without RLS. That policy is fine for genuinely public content, like a list of published blog posts, and wrong for profiles, orders or messages.
5. Storage buckets are not listable
Uploaded files (avatars, receipts, documents) live in storage buckets with their own policies. User documents belong in private buckets, readable only by their owner. Check that an anonymous visitor cannot list a bucket's contents.
Auth
6. Sign-up settings match your policies
If a policy allows "any authenticated user", then anyone who can create an account gets that access. Decide whether sign-ups should be open, require email confirmation, and whether anonymous sign-ins should be allowed at all.
7. Admin checks are on the server
Generated code sometimes guards the admin page with a check like user.email === "owner@..." in React. That hides the page but not the data. Store roles in the database (a table users cannot write to, or app_metadata) and enforce them in RLS policies or server code. Never compare a password in frontend JavaScript; the password ships with the bundle.
Server functions
8. Every function checks the caller
Server or edge functions that send email, charge cards, call AI models or write data must verify who is calling. For Supabase edge functions, the built-in JWT check accepts the public anon key, so also load the user with supabase.auth.getUser() and reject requests without one.
9. AI calls go through the server, with limits
If the browser calls OpenAI, Anthropic or Gemini directly, the API key is public and anyone can spend your credits. A sign of this in code is dangerouslyAllowBrowser: true. Move the call to a function, check the user, and cap usage per user.
Deployment
10. No public source maps
Source maps (.map files) let anyone read your original source, comments included. Vite does not publish them by default, but check build.sourcemap in vite.config and try opening the main JS file's URL with .map appended.
11. Security headers
Ask your host to send at least:
Content-Security-Policywithframe-ancestors 'none'(orX-Frame-Options: DENY) against clickjackingStrict-Transport-Securityso browsers always use httpsX-Content-Type-Options: nosniff
On Netlify these go in a _headers file or netlify.toml; other hosts have equivalent settings.
12. Check again after every big change
Security is not a one-time step in AI-built apps. Each prompt can add a table, a function or a new key. Re-check the list above after major features, and especially after anything involving payments, uploads or admin features.
Quick summary
| Area | Good | Bad |
|---|---|---|
| Keys in bundle | Supabase anon/publishable, Stripe pk_ | service_role, sk_live_, AI keys |
| Tables | RLS on, owner-based policies | RLS off, or using (true) |
| Buckets | Private, owner-only | Anyone can list files |
| Admin | Role checked in DB/server | Email compared in React |
| Functions | User verified inside | Anyone with anon key can call |