September 8, 2026 · 11 min read · VibedSites team
Shipping safely when AI writes the code
When a model writes the code, your job moves from typing to reviewing. These are the review points that actually protect users: secrets, authorization at the point of the side effect, validation at the boundary, database rules, migrations, and a rollback you have practised.

AI can produce a working feature in minutes. It can also produce a feature that works for you and leaks everyone else's data, and both look identical in a browser. Shipping safely is the discipline of knowing which parts of a change deserve your full attention.
You do not need to read every line. You do need to read every line that touches four things: secrets, permissions, money, and stored data.
The risk moved, it did not disappear
Handwritten code fails in ways you feel immediately: it does not compile, the page is blank, the button does nothing. AI written code fails in ways that pass a demo: an endpoint that never checks who is calling it, a filter applied in the interface but not in the query, a key that ended up in the browser bundle.
So the review question changes. Not "is this code good", but "what could this do that I did not intend".
Four questions before anything merges
1. Who can call this? Every path that writes, deletes, pays, or emails. If the answer is "anyone with the address", that is the finding. 2. Whose data does it read? Is the owner check in the query, or only in the interface that hides the button? 3. What happens on bad input? Empty, enormous, wrong type, hostile. 4. How do I undo this? For code and for data.
Secrets never reach the browser
Anything the browser downloads is public, including code you thought was hidden. Service keys, admin keys, and provider API keys belong in server code that reads them at call time.
Checks worth running on every change that adds an integration:
- Search the built output for the first characters of any secret. Zero matches, or you have a leak.
- Confirm the browser exposed variables contain only publishable values.
- Confirm no server only helper is imported, even indirectly, by a screen.
Authorization at the point of the side effect
Hiding a page is not security. A hidden route is one typed URL away from being visible. The check has to live where the effect happens.
Three rules that hold up:
- Verify identity server side, on the server, from the request, not from anything the browser told you.
- Verify permission before doing the privileged thing, never after.
- Never use an admin level credential before you know who is calling. Elevated privileges are the last step, not the first.
The failure to look for: an endpoint that reads a user id out of the request body and trusts it. The caller controls that value. Take identity from the verified session only.
Validate at the boundary, once
Every request from outside gets parsed and validated at the edge with a schema, and the rest of the code works with the parsed result. Two habits that matter more than they sound:
- The rules in the interface, the schema on the server, and the database constraints must agree exactly. When the form says optional and the server requires it, users see a blank screen.
- Reject rather than coerce. Silently repairing bad input is how corrupt rows get created.
Database rules are the real fence
Application code is one client of your data. The database rules are what protect it from every client, including the next feature and the console. So: row level security on, policies written per role, and permissions granted deliberately.
Then test the fence from the outside:
- As a signed out visitor: can you read anything private?
- As one signed in user: can you read or write another user's rows by changing an id?
- As a moderator or admin: are the elevated abilities actually limited to that role?
If a table has no policy, it is not "open", it is broken. If it has one broad policy that always returns true, it is open. Both need fixing before ship.
Server side fetching of a URL a user gave you
Fetching a link a visitor typed is a privileged network action, not a convenience. Validating that a string is a URL says nothing about whether your server should request it. Block loopback, private ranges, link local, and internal metadata addresses, and check the destination again after every redirect.
Migrations are forward only, with a plan
Data changes are the ones you cannot casually undo. Before applying:
- Add columns before code needs them. Remove columns after code stops using them. Two deploys, never one.
- New columns are nullable or have defaults, so existing rows stay valid.
- Write down how to reverse it, or accept that you cannot.
- Never destructive on a live table without a copy of what you are about to lose.
Test what hurts, not what is easy
You do not need full coverage. You need the paths where being wrong is expensive:
- Unauthorized access to each write path: no session, wrong user, wrong role.
- The money path, if there is one, including the failure and the duplicate call.
- The boundary decoder, with a valid payload, a malformed one, and a partial one.
- Anything you have already broken once. That is the highest value test in the codebase.
Ship in a way you can survive
- Publish to a preview and click through the real flows as a signed out visitor and as a normal user, not just as yourself.
- Ship one change at a time, so the cause of a problem is obvious.
- Watch the first minutes: server errors, failed requests, the console.
- Know your rollback before you need it, and have done it once on purpose.
The pre ship checklist
- No secret in anything the browser can download.
- Every write path checks identity and permission server side.
- Every user supplied input validated at the boundary against one shared schema.
- Row level rules tested as a stranger, as another user, and as staff.
- Migration is additive, reversible in plan, and applied before the code needs it.
- The interface's rules, the server's rules, and the database's rules say the same thing.
- Errors shown to users are useful and never leak internals.
- Rollback path is known, and the first five minutes after publishing are monitored.
What actually breaks
In practice, in this order: a permission check that only existed in the interface, a schema and a form that disagreed, a cache key missing an input that changed the output, a migration that assumed empty tables, and a hidden page treated as a locked one. None of these are exotic. They are all caught by the questions above, before a user finds them.
Ship happens. Ship it on purpose.
Comments
Sign in to join the discussion on this article.
Loading the conversation…