Vibe coding without the blast radius.

Describing what you want and letting a model build it genuinely works now. You can go from an idea to something running in an afternoon, and for a lot of small projects that is the correct way to work.
It is also very good at producing code that runs correctly and is quietly unsafe. Not because the model is careless — because make the form work and make the form work when somebody hostile finds it are two different requests, and only one of them usually gets made.
What follows is the short list of things that actually go wrong, and what closing each one looks like.
Why this happens
A model optimises for the request in front of it. Asked to build a contact form, it builds a working contact form. It has no way of knowing whether the repository is public, whether that key is the production one, or whether the endpoint will be found by an automated scanner within hours of going live.
Security work is mostly about the requests nobody thought to make. That is the whole gap, and it closes as soon as you start making them.
Hardcoded keys, and what a public repo really means
The most common one: the key goes straight into the file, because that makes it work immediately.
What people underestimate is how quickly that is found. Public repositories are scanned continuously by automated crawlers watching for key patterns in new commits. The gap between pushing a key and somebody using it is measured in minutes.
The second thing people underestimate is that git remembers. Deleting the key in a later commit does not remove it — it is still sitting in the history, readable by anyone who clones the repository. If a key has been committed, the fix is to rotate it at the provider. Removing the line is not a fix, it is a tidy-up.
Environment variable hygiene
The mechanics here are simple and almost entirely about doing them in the right order.
- Put .env in .gitignore before the first commit, not after. Retrofitting this is how keys end up in history.
- Commit a .env.example with placeholder values, so the shape of the configuration is documented without any of the secrets.
- Know which variables reach the browser. In most frameworks a prefix — PUBLIC_, NEXT_PUBLIC_ and so on — means the value is bundled into the client. A secret in a public variable is not a secret.
- Real values belong in the host’s environment settings, never in the repository.
- Validate them at build time. A build that fails loudly on a missing secret is much better than finding out when a customer submits a form and nothing arrives.
Form endpoints are public whether you advertise them or not
If your form posts to /api/contact, then /api/contact accepts requests from anywhere. Bots do not fill in your form and press the button — they post to the endpoint directly, which means every protection that lives in the page is simply not present.
This is why client-side validation is a convenience feature rather than a security control. Anything the browser checks can be skipped by not using a browser. It belongs there for the person typing, and it must also exist on the server for everyone else.
So validate every field server-side, and be specific about it. Length limits matter more than people expect: an unbounded message field is a straightforward way to fill your inbox or your database. Anything that comes from a fixed set — a service dropdown, a plan name — should be checked against that set on the server rather than trusted because the select element only offered three options.
Spam protection that does not punish real people
Layers work better than any single measure, and the cheapest layers are invisible.
A honeypot is a form field hidden from people and tempting to naive bots. If it comes back filled, discard the submission — but return an ordinary success response rather than an error. Telling a bot it failed is just telling it to try again differently.
Above that, an invisible challenge such as Turnstile or reCAPTCHA. The critical detail is that the token must be verified on your server, against the provider, on every submission. A widget that renders in the page but is never checked server-side is decoration, and it is a genuinely common mistake.
Worth remembering that visible CAPTCHAs cost real users something, and cost users with disabilities considerably more. Reach for the invisible layers first and add friction only if you actually need it.
Database tables without row-level security
This is the one with the largest blast radius, and it is specific to the way modern backends work.
Platforms like Supabase and Firebase expose an API over your tables directly, and the browser holds a key so it can talk to them. That is the intended design and there is nothing wrong with it — provided access rules exist. The key is not what protects the data. The rules are.
Without row-level security, any table the anonymous key can read is readable by anyone who opens developer tools and looks at the requests. This is not an exploit or a clever attack. It is the documented behaviour of a database that was never told who is allowed to see what.
In practice that has meant signup lists, order histories, and tables of customer names, emails and addresses sitting behind nothing at all — on sites that looked completely finished.
The fix is to deny by default and write a policy per table describing exactly who may read and write which rows. The mistake that hides the problem is testing while signed in as yourself, usually as the owner, where everything correctly works. Test as an anonymous visitor.
The pre-launch pass
None of this takes long once you know to look. Before anything goes live:
- Search the repository history for keys, not just the current files.
- Confirm .env is ignored and no real secret is committed anywhere.
- Post to each API endpoint directly, with missing and oversized fields, and check it refuses properly.
- Confirm the spam token is verified server-side, not merely rendered.
- Open the site logged out and in a private window, and look at what the network requests return.
- For every database table, ask what an anonymous user can read. Then check rather than assume.
This is not an argument against building this way
The speed is real and worth keeping. We build this way, and the earlier piece on what AI changed for small business web design is the other half of this argument.
The gap is only ever that working and safe to expose are two different bars, and by default you get the first one because it is the one you asked for. So ask for the second explicitly. What happens if this endpoint is posted to directly? What can be read with the public key? What of this ends up in the browser bundle?
The answers tend to be good. They just will not be volunteered.