Production Risk Audit — Sample Report
Application: Loopin — client portal + invoicing Platform: Lovable (React front-end · Supabase · Stripe) Stage at audit: live ~6 weeks · ~340 users · first paying customers just arrived Access provided: repository (read-only) + the running application, observed as an ordinary user Report date: 4 August 2026 Classification: Confidential Prepared by: Vibe2Prod
⚠️ THIS IS A DEMONSTRATION REPORT — NOT A REAL CLIENT
“Loopin” is a fictional app. This document is a composite — it stitches together the findings we see most often in AI-built (“vibe-coded”) apps into one realistic example, so you can see exactly what a Production Risk Audit looks like before you buy one.
No real company was audited. No real user data was touched. Every finding, commit, and line of evidence below is invented to be typical, not real.
Your real report would look and read exactly like this — but it would be about your app, with your evidence.
How to read this report
A published method reads the code and configuration your AI tools produced, plus the running app as any of your users would see it. What follows is a plain list of what it found, ranked by how much it would hurt you, with the fix for each — reviewed and signed by a senior engineer before it reached you.
A few things to know before you read on:
- We report findings. We do not give a verdict. You will not find a “readiness score” or a ready / not-ready call anywhere in this document. That call requires testing your live system and understanding your business, and it belongs to a different, larger engagement (see What this audit is not). Withholding it here is deliberate, not an oversight.
- Severity is a count, not a grade. We tell you how many Critical, High, Medium and Low findings there are. We never compress that into a single number, because a single number invites you to read it as “am I safe?” — and that is the question this audit does not answer.
- Read-only means read-only. We looked; we did not touch. We did not exploit anything, generate load, or pull real data. Everything below is phrased as what we saw, because that is all we did.
- Plain English, on purpose. You do not need to be technical to act on this report. Where a finding can be fixed with the same AI tools that built the app, we give you a prompt you can paste into Lovable or Cursor, plus one line telling you how to check the AI actually fixed it.
- Start at the top and work down. Findings are ordered by severity. The 30-day priority order near the end turns them into a sequence you can follow.
At a glance
Severity count
| Severity | Count | What it means |
|---|---|---|
| Critical | 3 | Live now, easy to trigger, and the damage lands on you or your users directly. Fix before you take another payment. |
| High | 4 | Serious. Realistic to trigger and costly if it does. Fix this month. |
| Medium | 4 | Real weaknesses that widen your exposure. Fix once the Criticals and Highs are closed. |
| Low | 2 | Hygiene and hardening. Worth doing; not urgent. |
| Total | 13 |
The five recurring flaws — which ones we found in Loopin
These are the five failure patterns we look for first in every AI-built app. All five are present in Loopin.
| The flaw | Found? | Where |
|---|---|---|
| Hardcoded keys — secret keys shipped to the browser | ✅ | PRA-02 |
| RLS disabled — the database does not enforce who can read what | ✅ | PRA-01 |
| Missing input validation — the server trusts whatever the browser sends | ✅ | PRA-05 |
| Broken auth logic — access is checked in the UI but not enforced on the server | ✅ | PRA-03 |
| Console / data leakage — user data and tokens printed where anyone can read them | ✅ | PRA-08 |
Beyond the five, we also found: a payment webhook that accepts unsigned requests (PRA-04), live keys still recoverable in git history (PRA-06), known-vulnerable and abandoned dependencies (PRA-07), no rate limiting on login (PRA-09), no evidence of backups (PRA-10), and no way to roll back a bad deploy (PRA-11).
Findings
Each finding carries: an ID, its severity, what we saw, what it costs you if it triggers, and how to fix it. Where the fix can be done with your own AI tools, a paste-ready prompt follows, with one line on how to confirm it worked.
Critical
PRA-01 · The database does not enforce who can read what (RLS disabled)
Severity: Critical · Flaw: RLS disabled
What we saw. In your Supabase schema, Row Level Security (RLS) is turned off
on the tables that hold customer data — profiles, clients, projects,
invoices, invoice_line_items, payments, messages and files. With RLS
off, the database itself places no restriction on which rows a signed-in user
can read. The front-end fetches “the current user’s invoices” by filtering in
the browser, but nothing on the server side stops a signed-in user from asking
for everyone’s invoices instead.
What it costs you if it triggers. Any one of your ~340 users — or anyone who signs up — can read every other customer’s clients, invoices, amounts, and messages by changing a value in a request. For an invoicing product this is the worst kind of exposure: your customers’ revenue figures and their clients’ details, readable across accounts. One screenshot from an upset user ends the trust you have with the rest.
How to fix it. Turn on RLS for every table holding user data, and add
policies so each row is only reachable by the account that owns it. Never use
the Supabase service_role key in browser code (see PRA-02).
Fix prompt (paste into Lovable / Cursor):
In our Supabase project, enable Row Level Security on every table that holds
customer data: profiles, clients, projects, invoices, invoice_line_items,
payments, messages, files. For each table, add policies so a signed-in user can
only SELECT, INSERT, UPDATE or DELETE rows that belong to their own account
(match on the owner/user_id column, or via the workspace they belong to). Deny
all access to anonymous requests. Do not reference the service_role key anywhere
in client-side code. Show me the SQL for each policy before applying it.
After the AI applies it, check: sign in as one test user and confirm you can no longer load another user’s invoices by changing the ID in the URL or the request.
PRA-02 · Secret keys are shipped to the browser (hardcoded keys)
Severity: Critical · Flaw: Hardcoded keys
What we saw. In the front-end code we read a Stripe secret key
(sk_live_…) and the Supabase service_role key assigned to VITE_-prefixed
variables. In a Vite app, anything prefixed VITE_ is compiled into the
JavaScript that ships to every visitor’s browser. We confirmed both keys are
referenced from client components — meaning they are downloaded by anyone who
loads the site and can be read straight out of the page source.
What it costs you if it triggers. These two keys are the master keys to your money and your data. The Stripe secret key can create charges and refunds and read your customers’ payment records. The Supabase service_role key bypasses all database rules (including any you add for PRA-01) and can read or delete every table. Because they are already in the browser, you must assume they are already public.
How to fix it. Move every secret key out of browser code and into a server
(a Supabase Edge Function). The browser should only ever hold the Stripe
publishable key (pk_…) and the Supabase anon key. Then rotate both leaked
keys.
Fix prompt (paste into Lovable / Cursor):
Find every place in this project where a secret key is used in front-end
(browser) code — specifically the Stripe secret key and the Supabase
service_role key. Move all of these into a Supabase Edge Function (server side)
that the browser calls. The browser must only use the Stripe publishable key
(pk_...) and the Supabase anon key. Remove any VITE_-prefixed variable that
holds a secret. List every file you changed.
After the AI applies it, check: open the deployed site, view the page source /
the network tab, and search the JavaScript for sk_live and service_role —
you should find neither. Then rotate both keys in the Stripe and Supabase
dashboards, because anything shipped to a browser must be treated as already
leaked.
PRA-03 · The admin area is locked in the UI but open on the server (broken auth logic)
Severity: Critical · Flaw: Broken auth logic
What we saw. The admin dashboard (/admin) is guarded by a check on a role
value held in the browser — a flag in the app’s state, seeded from
localStorage. The Supabase queries and actions behind that screen do not
re-check the user’s role on the server. In other words, the lock is on the door
of the room, but the room has no wall: the same data and actions are reachable
by calling the API directly, or by changing the flag in the browser, without
ever being an admin.
What it costs you if it triggers. Any signed-in user can reach admin-only data and actions — the full customer list, other people’s invoices, and any administrative controls you have built. Auth that only exists in the UI is one of the most common and most damaging patterns in AI-built apps, because the app looks protected in every normal test.
How to fix it. Enforce the role check on the server — in RLS policies and in any Edge Functions — reading the role from the database, not from a value the browser sends. Keep the UI guard for a clean experience, but never let it be the only thing standing between a user and admin data.
Fix prompt (paste into Lovable / Cursor):
Our admin area is protected only in the front-end React code. Move the
authorization check to the server: adjust the Supabase RLS policies and any Edge
Functions so admin-only data and actions verify the user's role from the
database, not from a value sent by the browser. The front-end route guard can
stay for UX, but it must not be the only thing enforcing access. Show me which
policies/functions now enforce the admin check.
After the AI applies it, check: as a normal (non-admin) user, call an admin API directly, or flip the role flag in the browser, and confirm the server refuses with an authorization error.
High
PRA-04 · The payment webhook accepts unsigned requests (webhook without signature verification)
Severity: High
What we saw. We read the Supabase Edge Function that receives Stripe
webhooks. It parses the incoming JSON and updates invoice and payment status
from it, but it does not verify the Stripe-Signature header against your
webhook signing secret. Nothing confirms the request actually came from Stripe.
What it costs you if it triggers. Anyone who learns the webhook URL — it is visible in network traffic — can send a made-up “payment succeeded” event and mark an invoice paid without any money changing hands. For an invoicing product that turns your payment status into something anyone can forge. You would ship work, or release a client portal, against a payment that never happened.
How to fix it. Verify every webhook’s signature before doing anything with it, and reject anything that fails.
Fix prompt (paste into Lovable / Cursor):
Our Stripe webhook Edge Function updates payment status but does not verify the
request came from Stripe. Add signature verification using
stripe.webhooks.constructEvent with the STRIPE_WEBHOOK_SECRET before any
database update. Keep the raw request body for verification (do not JSON-parse
before verifying). Reject any request whose signature does not validate with a
400 response and stop processing.
After the AI applies it, check: send a plain POST with a fake body to the webhook URL — it should be rejected, while real Stripe payments still update invoice status.
PRA-05 · The server trusts whatever the browser sends (missing input validation)
Severity: High · Flaw: Missing input validation
What we saw. In the code path that creates and edits invoices, we found no server-side checks on the submitted values. Amounts, quantities, tax rate, notes, and the client’s email are written to the database as they arrive from the browser. The React form does some checking, but the server accepts values that bypass the form — including negative amounts and very long text fields.
What it costs you if it triggers. Bad or hostile data ends up in your records: negative or absurd invoice totals, broken tax figures, oversized fields that corrupt exports or reports. Because the form is the only guard, anyone calling the API directly skips it. At best you get messy books; at worst a malformed value breaks a downstream feature for a paying customer.
How to fix it. Validate every write on the server, not just in the form.
Fix prompt (paste into Lovable / Cursor):
Add server-side validation to every write path that accepts user input,
especially invoice creation and editing. Using zod (or similar), validate that
amounts and quantities are positive numbers within sane limits, tax rate is
between 0 and 100, email fields are valid emails, and text fields have length
limits. Reject invalid input with a clear error before it touches the database.
Do this in the Edge Function / server code, not only in the React form.
After the AI applies it, check: try to create an invoice with a negative amount or a 10,000-character note — the server should reject it, not save it.
PRA-06 · Live keys are still recoverable in git history (secrets in git history)
Severity: High
What we saw. Reading the repository’s history, we found an early commit that
added a .env file containing live Stripe and Supabase keys. A later commit
removed the file, but git keeps history: the values are still present in earlier
commits and can be recovered by anyone with access to the repository.
What it costs you if it triggers. Deleting a secret from the current code does not delete it from history. Anyone you have ever given repository access — or anyone who obtains a copy — can read those keys and use them exactly as in PRA-02. Removing the file created a false sense that the problem was solved.
How to fix it. The essential fix is not a code change: rotate every key that was ever committed, in the Stripe and Supabase dashboards, and treat the old ones as public. Then stop it recurring and scrub the history.
Fix prompt (paste into Lovable / Cursor):
Add a .gitignore that excludes .env and any *.env.* files, and confirm no
secrets remain in the current working tree. Then give me the exact commands to
remove the previously-committed .env from the entire git history using
git filter-repo (or BFG Repo-Cleaner), and how to force-push the cleaned
history.
After the AI applies it, check: search the whole history (git log -p -- .env)
and confirm the file’s contents no longer appear. Remember: scrubbing history
only limits future exposure — rotating the keys is what actually closes the
door.
PRA-07 · Known-vulnerable and abandoned dependencies (vulnerable / abandoned dependencies)
Severity: High
What we saw. Reading package.json and the lockfile, we found several
direct dependencies with published security advisories, and one image-handling
library with no release in over two years and open unpatched issues. Details are
in Dependency & supply-chain picture below. These are third-party packages
your app pulls in and runs.
What it costs you if it triggers. Vulnerable dependencies are borrowed risk: a flaw in a package you did not write becomes a flaw in your app. Abandoned packages are worse over time — when a problem is found, no fix is coming, and you are left carrying it. Because AI tools tend to pin whatever version existed when they wrote the code, this quietly drifts further out of date every week.
How to fix it. Update the flagged packages carefully, replace the abandoned one, and test the main flows after each change.
Fix prompt (paste into Lovable / Cursor):
Audit our dependencies for known security advisories and unmaintained packages.
Upgrade each flagged package to the nearest version that clears the advisory,
preferring non-breaking updates. Where a package is abandoned, suggest a
maintained replacement. Start the app and run any tests after each change and
tell me anything that breaks so we can decide together. Do not bulk-upgrade
blindly.
After the AI applies it, check: run your package manager’s audit command (e.g.
npm audit) and confirm no High or Critical advisories remain; then click
through login, create-invoice and take-payment to confirm nothing broke.
Medium
PRA-08 · User data and tokens are printed to the browser console (console / data leakage)
Severity: Medium · Flaw: Console / data leakage
What we saw. Opening the running app as an ordinary user with the browser
console open, we saw console.log output containing the signed-in user’s full
profile, the Supabase session object (including the access token), and complete
invoice records as they loaded. These logs were left in from development and
ship in the live app.
What it costs you if it triggers. Anything printed to the console is readable by the person at the keyboard and by browser extensions they have installed. A leaked session token can let someone act as that user until it expires. Even without a token, printing customer data to the console is a quiet privacy leak you would never see in a screenshot of the working app.
How to fix it. Strip data-bearing console output from the front-end and add a rule so it does not come back.
Fix prompt (paste into Lovable / Cursor):
Remove all console.log / console.debug statements that print user data, tokens,
session objects, API responses, or environment values from the front-end code.
Keep only intentional error logging that contains no personal data or secrets.
Add an ESLint rule (no-console, allowing only console.error and console.warn) so
new ones are caught. List every file you changed.
After the AI applies it, check: open the deployed app with the console open, sign in and use the main features — no tokens, sessions, or other data should appear.
PRA-09 · Login has no rate limiting (missing rate limiting)
Severity: Medium
What we saw. We found nothing in the code or configuration that limits repeated attempts on the login, signup, and password-reset paths. The same request can be sent as fast and as often as a script allows.
What it costs you if it triggers. Without a limit, an attacker can try thousands of passwords against your users’ accounts (credential stuffing), or hammer the password-reset flow to flood a person’s inbox and burn your email sending reputation. For a product holding invoices and client relationships, one guessed account is one customer’s data gone.
How to fix it. Add limits per IP and per account on the auth paths and on any function that sends email.
Fix prompt (paste into Lovable / Cursor):
Add rate limiting to our authentication paths — login, signup, password reset —
and to any Edge Functions that send email or create records. Limit repeated
attempts per IP and per account (for example, a small number of attempts per
minute, then a short lockout), returning a 429 response when exceeded. Use
Supabase's built-in auth rate-limit settings where available and implement
limiting in Edge Functions for the rest. Tell me which limits you set.
After the AI applies it, check: enter a wrong password many times quickly — after a few tries you should be temporarily blocked (429), not allowed to keep guessing.
PRA-10 · No evidence that backups exist (no backup evidence)
Severity: Medium
What we saw. In the repository and the configuration we could read, we found nothing that shows database backups are configured or tested — no backup schedule, no export job, no restore procedure or runbook. We could not see your Supabase dashboard, so we cannot confirm whether the platform’s own automatic backups are enabled (see What we could not see). What we can say is that nothing you control documents a way to recover your data.
What it costs you if it triggers. A bad migration, a wrong delete, or a
mistaken bulk edit could permanently remove customer data. Without a known,
tested backup, “restore from yesterday” may simply not be an option — and you
would find that out at the worst possible moment, in front of paying customers.
How to fix it. This one is settings and process, not code — there is no useful AI prompt:
- In Supabase, confirm what your plan actually provides (automatic daily backups and/or point-in-time recovery) and that it is switched on.
- Add a scheduled export of critical tables to storage you control.
- Do one real restore into a scratch project so you know the backup works and how long recovery takes. A backup you have never restored is a guess.
PRA-11 · No way to roll back a bad deploy (missing rollback path)
Severity: Medium
What we saw. The project deploys from a single branch straight to a single live environment. We found no separate staging environment, no tagged releases, and no documented way to return to a previous known-good version if a deploy breaks something.
What it costs you if it triggers. When a deploy goes wrong, it is your live, paying customers who hit the breakage — and your only option is to fix forward under pressure while the app is down or misbehaving. A rollback path turns a crisis into a one-click “undo” and buys you time to fix things calmly.
How to fix it. This is infrastructure and process, not code:
- Use your platform’s deployment history so you can re-publish the previous working version quickly; note down exactly how to do it.
- Add a staging environment and make changes there first.
- Tag or note each release so “the last good one” is always identifiable.
Low
PRA-12 · Security response headers are missing
Severity: Low
What we saw. Observing responses from the running app, we saw that common
security headers are absent — no Content-Security-Policy, no
Strict-Transport-Security, no X-Frame-Options / frame-ancestors, no
X-Content-Type-Options.
What it costs you if it triggers. These headers are defense-in-depth. Without them your app can be embedded invisibly inside another site (a clickjacking setup that tricks users into clicking things), and you lose several cheap protections browsers give you for free. Low on its own, but easy to close.
How to fix it. Add the standard headers in your hosting configuration.
Fix prompt (paste into Lovable / Cursor):
Add standard security response headers to our app: Content-Security-Policy
(start in report-only mode), Strict-Transport-Security, X-Content-Type-Options:
nosniff, Referrer-Policy, and a frame-ancestors / X-Frame-Options rule to stop
the site being embedded in other sites. Configure these in our hosting /
deployment settings. Show me the header values you set.
After the AI applies it, check: load the site and inspect the response headers in the browser network tab — the headers above should be present, and the app should still load normally with the CSP in report-only mode before you enforce it.
PRA-13 · Weak password requirements at signup
Severity: Low
What we saw. Signing up as an ordinary user, we were able to create an account with a very short, simple password. We saw no minimum length or strength requirement enforced in the app or in the auth settings we could observe.
What it costs you if it triggers. Weak passwords make the missing rate limiting in PRA-09 more dangerous — easy passwords plus unlimited guesses is how accounts get taken over. Raising the floor is a small change with an outsized effect on account safety.
How to fix it. Enforce a minimum password policy in both the auth settings and the form.
Fix prompt (paste into Lovable / Cursor):
Enforce a minimum password policy at signup and password change: at least 10
characters, and reject the most common / breached passwords. Enforce it in the
Supabase auth settings and show a clear message in the React form. Do not rely
on client-side checks alone.
After the AI applies it, check: try to sign up with a password like 123456 —
it should be rejected with a clear message.
Dependency & supply-chain picture
We read package.json and the lockfile and looked at what Loopin actually pulls
in. This is not a full scan of every transitive package — it is what a senior
engineer notices on a careful read, and it maps to finding PRA-07.
| Area | What we saw | Concern |
|---|---|---|
| Direct dependencies with advisories | A handful of direct packages sit on versions with published security advisories (the app is pinned to whatever version existed when the code was generated). | These have known fixes available in later versions. |
| Abandoned package | One image-handling library has had no release in over two years and carries open, unaddressed issues. | If a flaw is found, no fix is coming; it needs replacing. |
| Version pinning drift | Dependencies are pinned to generation-time versions and have not moved since. | Security fixes released since launch have not been picked up. |
| Lockfile present | A lockfile is committed, which is good — it makes upgrades reproducible. | Use it: audit and upgrade against it deliberately. |
What this means in plain terms. Every package you install is code you now run and are responsible for. The problem here is not that Loopin uses a lot of packages — it is that they were frozen at build time and one of them is no longer maintained. Fixing PRA-07 with the prompt above closes most of this; the abandoned package needs a deliberate swap.
What we did not do here. We did not run a paid vulnerability scanner, resolve the full transitive dependency tree, or confirm whether any advisory is actually reachable in the way Loopin uses the package. A named list with severities and exploitability is part of the deeper Production Readiness Review.
What we could not see
This is the most important section for judging what the report does not cover. An audit is only honest if it names its own limits. For Loopin, we worked from the repository and the running app observed as an ordinary user — nothing more. That means the following were outside what we could see, and any risk in them remains unmeasured:
- No SaaS dashboards. We did not have access to the Supabase, Stripe, or hosting dashboards. So we could not confirm whether platform backups are on (PRA-10), what the live auth settings are, what secrets are stored server-side, or how the production environment is actually configured.
- No server or platform logs. We could not see runtime logs, error rates, or whether anything is already going wrong in production.
- No dynamic or intrusive testing. We did not exploit anything, run a penetration test, generate load, or attempt account takeover. Findings describe what the code and configuration make possible, read from the outside — not a confirmed break-in.
- No production data. We did not query, export, or view real customer data. Table and field references come from the schema and the code, not from records.
- No business context. We do not know which flows matter most to your revenue, what your customers were promised, or what a given failure would cost in your specific case. We describe technical exposure, not business consequence measured against your operation.
- Not exhaustive. No audit is. There may be issues a deeper, testing-based engagement would surface that a read-only review cannot.
Where a finding depends on something we could not see, we said so in the finding itself.
30-day priority order
A sequence you can actually follow. Do the groups in order; within a group, top to bottom.
Before you take another payment (this week)
- PRA-02 — get secret keys out of the browser, then rotate them.
- PRA-06 — rotate any keys ever committed to git (same rotation covers PRA-02).
- PRA-01 — turn on RLS and add ownership policies.
- PRA-03 — enforce the admin check on the server.
- PRA-04 — verify the Stripe webhook signature.
These five are the difference between “a user could read everyone’s data / forge a payment right now” and “they cannot.” They are the whole reason this group exists.
Week 1 (once the Criticals are closed)
- PRA-05 — validate invoice input on the server.
- PRA-07 — update vulnerable packages, replace the abandoned one.
Weeks 2–3
- PRA-08 — remove data and tokens from the console.
- PRA-09 — add rate limiting to login, signup, and password reset.
- PRA-10 — confirm and test a real backup and restore.
- PRA-11 — set up a rollback path and a staging environment.
Weeks 3–4 (hardening)
- PRA-12 — add security response headers.
- PRA-13 — enforce a minimum password policy.
A note on scope creep. Do not try to do everything at once. The first group is genuinely urgent; the rest is a steady month of work, and every item you close in order makes the next one safer.
What this audit is not
We want you to leave this report clear on one thing: we did not tell you whether Loopin is ready to launch, and we did not give it a score. That is on purpose.
- No verdict. There is no ready / ready-with-conditions / not-ready call anywhere in this document.
- No readiness score. No number out of ten. A single number that answers “can I launch?” is a verdict, and a verdict requires testing the live system and understanding your business — which read-only access does not allow.
- The ready / not-ready call is a separate product. It is the Production Readiness Review, sold separately. The Review tests your system, maps your critical flows, weighs business consequence, checks recovery for real, and ends in a written decision you can act on.
- Your audit fee is creditable. The full amount you paid for this audit is credited against a Production Readiness Review purchased within 30 days. If this report has shown you that you need the verdict, you do not pay twice for getting here.
- A 15-minute debrief is available on request. The written report stands on its own; if you want to walk through it live, just ask.
- Not a penetration test, certification, compliance opinion, or legal advice. It is a read-only engineer’s read of what your AI tools built, and nothing more.
Confidentiality note
- Read-only access, used for nothing else. The access you granted was used solely to produce this report.
- Code and credentials deleted after delivery. We do not retain your repository or any credentials once the report is delivered.
- Anonymized statistics only with your written permission. We keep aggregate finding counts only if you have explicitly allowed it in the authorization form — never otherwise.
- NDA available on request.
- No trackers, no sponsored anything. Not on our pages, not in this report.
This is a demonstration report about a fictional app named “Loopin.” It exists to show the shape, depth, and voice of a real Production Risk Audit. No real company, repository, or user was involved.