The short version
- Most of the worry resolves into seven checks, and you can run every one of them yourself. Nothing here needs me.
- The order is by irreversible harm. A leaked key or a readable database cannot be un-leaked; a clumsy deploy process is merely annoying. Start at the top and work down.
- Each check ends with the honest signal that it is bigger than a weekend — so you know what you are looking at early, rather than on Sunday night.
- Most readers will not need anyone. The three situations where an outside pair of eyes genuinely earns its place are at the end, and they are narrow.
Why you can probably do this yourself
AI coding tools fail in patterns. Veracode's 2025 study across 100+ models found AI-generated code chose the insecure implementation about 45% of the time, with no improvement in newer models (Veracode). That sounds like an argument for professional help. It is mostly the opposite: because the failures are patterned, the list of things worth checking is short, stable and public. What follows is not secret craft. It is the same list I work through on a paid review, in the same order, written out so that you can run it without me.
One ground rule before the list. When a check below says "verify", it means observe the behaviour of your own running app with your own test accounts — never anyone else's app, and never real users' data. Everything here is you checking your own locks.
1 — Get secrets out of the client bundle
Everything your app ships to the browser is public. Every JavaScript file it serves can be opened and read by anyone, with no skill involved beyond View Source. AI tools put privileged API keys straight into frontend code constantly, because it is the shortest path to "it works" — and the demo looks identical either way.
The check. Build the app, then search the build output folder for the
first few characters of every key in your environment file. Separately, list every
environment variable your framework exposes to the client — the VITE_,
NEXT_PUBLIC_ and REACT_APP_ prefixes all mean "shipped to the
browser by design" — and ask of each one: would I hand this to a stranger? Some keys are
built to be public: a Stripe publishable key
(Stripe docs)
and a Supabase anon key
(Supabase docs)
are designed to sit in the browser, with the real protection happening server-side. A
secret key, a service-role key, or an AI provider key is never in that category.
The fix. Move the call that needs the key behind a server endpoint — an API route or an edge function — and keep the key there. Then rotate any key that was exposed: deleting it from the code does not un-leak it. And check your git history, not just the current code — a key committed once and "removed" later is still in the history and still live. A history-wide scanner such as gitleaks or trufflehog does this in minutes.
Done when the built bundle contains only keys designed to be public, and every privileged key that ever appeared in the repository has been rotated since the day it last did.
Beyond a weekend when an entire feature calls a provider directly from the browser with a privileged key and there is no server tier to move it behind. That is re-architecture, not a fix, and it deserves a plan rather than a Saturday.
2 — Check authorisation on the server, for every write
The classic AI-app failure: the interface hides the delete button from the wrong users, and the endpoint behind the button accepts the request from anyone. Generated code checks permissions where they are visible — the UI — and not where they count — the server. Broken access control is the top category in the OWASP Top Ten (OWASP), and it is invisible in normal use because normal users only press the buttons they can see.
The check. List every action in your app that changes data — creates, edits, deletes, role changes, anything involving money. For each one, ask: what, on the server, stops one signed-in user doing this to another user's records? The answer must be a line of server code or a database policy you can point at. "The button is hidden" is not an answer. Neither is "the app never sends that request" — the browser belongs to the user, not to you, and requests can be sent without your interface.
The verification. Create two test accounts in your own app. Signed in as the second, try to view and change the first account's data through the app's normal screens. Every attempt should be refused by the server, not merely hidden by the layout.
The fix. Derive who is acting from the server-side session — never from an id the client sends — and check ownership or role before every write. It is usually a small amount of code in a small number of places; the work is the audit, not the typing.
Beyond a weekend when your app needs a permission model that does not exist yet — admin, member, viewer, teams — because designing roles is product work, not patching. And if your app has no server tier at all, with the client talking straight to the database, this whole question becomes the next check. Go there.
3 — Database policies that actually restrict rows
If your client talks directly to a hosted database — Supabase and Firebase apps both
work this way — then row-level policies are your entire authorisation layer, and
"enabled" is not the same as "correct". A Postgres policy of USING (true)
is switched on, shows green in a dashboard, and returns every row in the table to anyone
who asks. Supabase's own
database advisors
catch the crude cases — RLS disabled, enabled with no policy — but no automated check
judges whether a policy is right. That part is reading.
The check. For every table holding user data, read the policy and
confirm it names the relationship between the user and the row — "rows where
user_id equals the authenticated user's id", not "all rows". Supabase's
row-level security guide
shows what correct policies look like; Firebase's equivalent is its
security rules.
The verification. The same two-account test as the previous check — but remember your database API is reachable without your app's interface, using the same public credentials your own frontend ships. The policy has to hold on its own, not just behind your UI.
Beyond a weekend when you have many tables, shared or team data, or role hierarchies — policy-per-table across a real schema is patient work. And if the check reveals that real users' data has already been readable by strangers, stop and take that seriously: it may be a reportable breach, and the ICO's guidance on reporting is the place to start, not this page.
4 — A backup you have personally restored
"The platform does backups" is a sentence people believe on no evidence. Sometimes it is true; what your plan actually includes is written in your host's documentation — Supabase's is here — and it varies by tier. Either way, a backup you have never restored is a hope, not a backup.
The check and the fix are the same act. Take a backup — the platform's own export, or pg_dump for a Postgres database — restore it into a scratch project, and look at your data sitting there. Then write down the three things you just learned: where the backup lives, the exact steps that restored it, and today's date as the day you proved it.
Done when that note exists and the date on it is real.
Beyond a weekend: almost never. This is the quickest item on the whole list and the one most often skipped. If restoring fails, that is not a reason to hire anyone — it is the single most valuable thing this page could have told you, discovered on a quiet afternoon instead of during an outage.
5 — An error you can actually see
The default state of an AI-built app is that when something breaks for a user, nobody finds out. Generated code is fond of the silent catch block — the error is caught, nothing is logged, the user sees a spinner that never resolves, and the first you hear of it is an email, or more often silence and a lost user.
The check. Cause a deliberate failure in a test environment — a thrown error in a server function is enough. Can you find it five minutes later, without the user telling you? If the answer is no, you are running blind.
The fix. Add error monitoring — Sentry has free tiers and one-file setup for most frameworks, and your host's function logs are better than nothing — and route an alert somewhere you actually look. While you are in there, search the code for empty catch blocks and make each one either handle the error or report it.
Done when you can name the last three errors real users hit. If you cannot name any, that is not evidence of quality; it is evidence of blindness.
Beyond a weekend when the app swallows errors in many places and each one needs finding and rewiring — tedious rather than hard, but honest work measured in days.
6 — Dependency updates that do not break you
The symptom is a wall of security alerts you are afraid to act on, because there are no tests and the last time you updated something, the app broke in a way you found out about a week later. The fear is rational. The fix is not "be braver" — it is to make updating boring.
The fix, in order. First, write one smoke test: the single test that signs in, performs your app's core action, and sees the result. One honest end-to-end test converts updating from gambling into routine, and it is also the test that catches a hundred future mistakes that have nothing to do with dependencies. Then enable Dependabot so updates arrive as small pull requests instead of an annual reckoning, and take them in small batches — run the smoke test after each.
Done when a security update lands within days of being published, because taking it costs you ten minutes and no fear.
Beyond a weekend when you are a major framework version behind with no tests at all. That is a project — plan it as one, and write the smoke test first anyway, because it is what makes the project survivable.
7 — A deploy that does not depend on one laptop
If deploying your app means one specific person runs a command on one specific machine — or clicks a button inside one personal account — then that laptop is load-bearing infrastructure, and the person is a single point of failure who cannot safely go on holiday.
The fix. Deploy from the repository. Most hosts do this natively — connect the repo and a push to the main branch deploys. Anything they do not cover fits in a GitHub Actions workflow. The secrets your deploy needs move from a laptop's environment into the host's or repository's encrypted settings, which also quietly improves check number one.
Done when a push to the main branch deploys, and a second person has actually done it once — proved, not presumed.
Beyond a weekend when deploys currently involve hand-run database migrations and secrets copied between machines. Untangling that is real work — and it is also exactly the tangle that eventually loses somebody's data, so it belongs on your list even if not on your weekend.
Where an outside pair of eyes earns its place
Three situations, and only three, where I think paying someone to look genuinely makes sense:
- The app moves money or holds genuinely sensitive data — payments, health information, children's data — where a mistake is not recoverable by shipping a fix next Tuesday. The cost of being wrong is asymmetric, and asymmetry is what reviews are for.
- Someone else is about to rely on your answer. A customer contract, a due-diligence question, a launch that takes on real users' data. "I ran a checklist I found online" is a true sentence, but the situation calls for a named person who has read the code and put their name to an opinion.
- You have run the checks and cannot tell what you are looking at. Not "it is broken" — "I cannot determine whether this policy is right". Uncertainty you cannot resolve is itself a finding, and it is the one finding on this page that self-service cannot clear.
Most readers of this page are not in any of the three. If the checks above pass, you do not need us — you never did, and that is the happiest outcome this page has. It is also not a loss leader: an app whose owner has restored a backup and read their own policies is simply a better app, whoever checked it.
If one of the three does describe you: how a review works is on the audit page, and who I am is at lukeczak.com. That is the whole pitch.