Vibecoders breaking production
Thirteen checks before you push anything to production. The same ones I run, ordered by how much it hurts if you skip them.
Contents
You have the app finished and your finger over the ship button. This is the list I go through before hitting it, ordered by how much it hurts if you skip it.
This is not a list of best practices for some day: it is twelve concrete checks, each one with what to look at and how to look at it. At the end there is a prompt you hand to your agent and it comes back with the whole review done on your code.
1. Test the critical flows end to end
A critical flow is the one that, if it breaks, your app is worth nothing: getting in, doing what it promises and, if you charge, paying. Testing it end to end means walking the whole thing as somebody arriving from scratch would, not checking that each piece works on its own.
And do it on a computer that is not yours, or at least in an incognito window and with another account. Almost everything that falls over on launch day worked perfectly on the machine of whoever wrote it, because there the session was already open, the cache was warm and the data was the usual data.
2. Test login, signup and password recovery
There are four paths, not one: signing up with an email that has never been used, logging in, logging out and logging back in, and recovering the password. The fourth is the one that always breaks, because it is the only one that depends on an email leaving your server and actually arriving somewhere else.
Check the boring parts too: that the recovery link expires, that it can only be used once, and that logging out invalidates the session on the server and not just clears a cookie in the browser. And try signing up with an email that already exists: that has to say something clear, not blow up.
3. Review roles and permissions
If your app has more than one type of user, you have a table to test: every route by every role. And there is one cell almost everybody skips, which is a normal user typing the admin panel address by hand. Not showing them the link does not mean they cannot get in.
What matters is where it is checked. Hiding a button in the browser is not a permission: it is make-up. The permission is checked on the server, on every request, and if it is not there, it does not exist. Test what happens when you take a role away from somebody while their session is open too.
4. Check the environment variables
Your laptop has a .env that has been filling up for weeks and that you no longer remember in full. Production has none of that until you put it there by hand, variable by variable, and one missing is enough for the app to start halfway and fail in the strangest place.
Make it refuse to start if a required one is missing. It is one check at the beginning of the process that stops the deploy with a clear message, and it saves you finding out through a 500 at three in the morning. Check that the keys are the real ones and not the test ones too: sk_test and sk_live look very alike at eleven at night.
5. Not one key published, anywhere
Keys live in your hosting's variable manager and nowhere else. The .env in .gitignore, a .env.example with the names and none of the values, and that is it. Everybody knows this and it is still how they get in, because the problem is not the key you write: it is the one that slips out without you writing it.
The three places it slips out: in whatever reaches the browser, because any variable that travels to the client is public by definition; in the deployment logs, which print whole environments; and in the screenshot of the dashboard you post on social. And if it was published, it does not get deleted: it gets rotated. What enters Git history stays in Git history.
6. Test payments and webhooks for real
The charge does not end at the payment provider: it ends when your server receives the notification, checks it comes from who it says, and gives the user access. That notification is the webhook, and it is the piece nobody tests because locally it does not arrive on its own. You have to forward it with the provider's tool, and you have to verify the signature every time.
Test the three cases, not just the good one: the payment that works, the card that gets declined and the notification that arrives twice. Providers retry when you do not answer fast, so if your code is not idempotent you will hand out two months of access, or charge twice. And make at least one real one-euro charge before opening.
7. Test the errors, the loading and the empty state
Every screen that asks for data has four states and you have only looked at one. There is the one loading, the one with data, the one with none and the one that failed. The one with data is the only one you see while developing, because your database has been full for months and your network is instant.
The empty one is the most important of all: it is the first one every new user sees. If it says “nothing here”, you are wasting the best moment to tell them what to do. And the error has to offer a way out —retry, go back— rather than leaving somebody staring at a blank screen. Test the loading with the network throttled from the browser.
8. Check mobile properly
You built it on a laptop and they are going to open it on 390 pixels of width, especially if they arrive from social. Narrowing the browser window is not enough: there are things you only see on a real phone, like the keyboard covering the field you are filling in, or the area you can actually hit with your thumb.
Look at what always breaks: wide tables that put a horizontal bar across the whole page, buttons stuck to the bottom edge where the browser bar lives, text that overflows and modals you cannot close. And open it in Safari on an iPhone, which is still the one that behaves differently.
9. Remove debug mode, mocks and test data
Every app built in a hurry has its graveyard: the /test route you put together to get by, the “John Smith” user with a one-euro price, the console.log that prints the whole user object and, worst of all, the flag that skips the login “just while I am developing”.
Do not look for it from memory, look for it with the editor's search: TODO, FIXME, test, mock, dummy, localhost, console.log. And turn debug mode off in production, because a detailed error on screen tells whoever is watching how your app is put together inside.
10. Turn on logs and error alerting
If you have nothing set up, your alerting system is a user bothering to write to you. And most do not write: they close the tab and do not come back. You need two things, and both have free plans: logs you can read and a tool that captures errors with their stack trace and tells you.
Make the alert land where you actually look, be it email or Slack. And when you store the error, store which user it was and what they were doing with it, but without putting passwords or tokens inside, which is exactly point five of this list repeating itself through the back door.
11. Set up backups
Automatic, daily and stored somewhere else, not next to the database that can go down with it. Almost every managed service ships with them, but “shipping with them” and “working” are different things until you check it yourself.
And here is what almost nobody does: restore one, just once, with a stopwatch in your hand. Until you do, you do not know the two things you have to know before you need it, which are how much data you could lose and how long it takes you to be back on your feet.
12. Have the rollback ready
You are going to deploy something that breaks the app. That is not pessimism, it is statistics. The difference between a two-minute scare and a hellish afternoon is whether going back to the previous version is a button you already know where to find, or something you look for the first time while people are complaining.
Find out today how to roll back on your hosting and try it once with the app running. And if you have database migrations, check the previous version still works with the new schema, because the code goes back in one click and the data does not.
13. Let the machine check it: pre-commit hooks
Everything above depends on you remembering, and you are not going to remember. A pre-commit hook is a script Git runs before letting you save the change, and if it fails, there is no commit. It is the difference between a list you read and a list that enforces itself.
In Node projects the usual choice is Husky, which in version 9 installs with one command and leaves the hooks in a .husky folder that does get committed, so everyone who clones the repo gets them. It is almost always paired with lint-staged, which is what makes the checks run only on the files you touched rather than on the whole project.
The alternative gaining the most ground is lefthook: it is a Go binary, it does not depend on Node, it is configured in a YAML file and it runs tasks in parallel, which shows on big projects. It needs a lefthook install the first time, because it does not hook itself up when you install dependencies. And if your project mixes languages, pre-commit is still the one that handles that best.
What matters is not which one you pick, it is what you put inside. The pre-commit gets whatever takes seconds: formatting, the linter on what you touched, types if it is worth it, and a search for keys and console.log. The full test suite and the build go in the pre-push or in CI. If the pre-commit takes half a minute, your team discovers --no-verify within two days and it stops protecting you from anything.
Do not go through it from memory: hand it to your agent
Twelve points are not something you remember at eleven at night with the itch to publish. That is why the button below is here: it is this whole list turned into a job your agent understands.
You paste it into Claude Code, into Cursor or whichever one you use, and it hands you back a report point by point, with the file and the line, ordered from most to least serious and without touching anything until you say so. And whatever comes back red, you already know where to look, because it is above.