Sample report. The app and every finding are invented. The page was produced by the same code that writes real reports, so this is what you receive. Back to the App Audit
App Audit · Impossible Means Go
What we found in your app's code
sample-owner/member-portal · prepared for Sample Coaching Co. · read on 26 October 2026
We found 34 problems in your app.
23 are serious: 9 critical and 14 high. Fix these first.
- 9 critical
- 14 high
- 8 medium
- 3 low
- 3 areas clean
- 1 not checked
- Critical
- Someone could get in, take data, or cost you money now. Fix before anything else.
- High
- A real hole, or a wrong result that will reach customers. Fix this week.
- Medium
- Breaks under normal use, or leaves you blind when it does.
- Low
- Worth fixing when you next work on that part.
Fix these first
| # | Severity | What's wrong | How to fix it |
| 1 | Critical |
Your Stripe secret key is still in the project's history src/config.ts (removed in commit 4e1a9c2) | Create a new secret key in Stripe, put it in your hosting settings instead of the code, then delete the old key in Stripe. |
| 2 | Critical |
Your database master key is sent to every visitor's browser .env (VITE_SUPABASE_SERVICE_KEY), used in src/lib/admin.ts line 6 | Create a new service key in Supabase, keep it only on the server, and delete the old one. |
| 3 | Critical |
Your OpenAI key is in the code every visitor downloads src/lib/ai.ts line 3 | Create a new key, call the AI only from a server function, and delete the old key. |
| 4 | Critical |
Any signed-in member can read every other member's records src/lib/supabase.ts line 14 | Turn on row level security for the members table and add a rule that each member reads only rows where user_id matches their own. |
| 5 | Critical |
Anyone signed in can open any order by changing the number in the address api/orders/[id].ts line 8 | Load the order only if it belongs to the signed-in member. |
| 6 | Critical |
The chat assistant will hand any member another member's records api/chat.ts line 18 | Give the chat only the signed-in member's own rows, chosen on the server. |
| 7 | Critical |
Anyone can sign up as an admin api/signup.ts line 15 | Save only the fields sign-up needs, and set the role on the server. |
| 8 | Critical |
Members' uploaded files sit in a public folder supabase/migrations/0003_storage.sql line 4 | Make the folder private and hand out short-lived links only to the member who owns each file. |
| 9 | Critical |
Card numbers are saved in your own database as plain text src/pages/Checkout.tsx line 51 | Stop saving card details anywhere. Let Stripe's own form collect them, and delete what is stored. |
| 10 | High |
The admin page checks the role in the browser, not on the server src/pages/Admin.tsx line 22 | Check the admin role inside the server function that returns the data, and refuse the request when it is missing. |
How someone could get in
Our attacker pass took the problems in this report and worked out how someone could combine them, reading the code that connects each step.
1. Any member downloads every other member's details and payments
They start with: A free account on your app
- They sign up and open the browser's developer tools.
- They ask the database for the whole members table, and get every member's name, email and files back.
- They change their own role to admin in the browser and open the admin data, which never checks.
What it costs you: Every customer's details and every order and refund leave your app, and you must tell all of them.
2. A stranger takes over your whole database without signing up
They start with: Your public website, and nothing else
- They view the page source and copy the database master key from it.
- With that key they read every member, every order and every uploaded file's address.
- They open the uploaded files, which need no sign-in, and change prices in the database while they are there.
What it costs you: Your members' documents are public, your records can be changed or wiped, and you would not know it happened.
3. A stranger makes themselves an admin and takes your programs for free
They start with: The sign-up form
- They sign up with one extra field, role set to admin, and the app saves it.
- As an admin they read every order and every stored card number.
- They send your payment webhook a fake "paid" message and unlock any program they like.
What it costs you: Your customers' cards are stolen, your programs are given away, and the card networks can stop you taking payments.
Reading code cannot prove an app is free of problems: it cannot see your live settings, your hosting dashboard or your database. Each of the 19 areas below says what we found, that we checked it and found nothing, or that we could not check it and why.
1. Secrets
3 problems
CriticalYour Stripe secret key is still in the project's history
- What's wrong
- A Stripe secret key was typed straight into a settings file. It was later deleted from the file, but every earlier version of the file is kept in the project's history, and the key is still readable there.
- Why it matters
- Anyone with that key can issue refunds and read every payment your business has taken. Deleting it from the file did nothing, because it still works.
- How to fix it
- Create a new secret key in Stripe, put it in your hosting settings instead of the code, then delete the old key in Stripe.
- Where
src/config.ts (removed in commit 4e1a9c2)
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
My app reads its Stripe secret key from src/config.ts. Change it so the key is read only from an environment variable called STRIPE_SECRET_KEY, and make the app refuse to start if that variable is missing. Do not put any key in the code.
- How to check it worked
- Search the project for "sk_live". Nothing should be found, and the app should still take a test payment after you add the new key in your hosting settings.
CriticalYour database master key is sent to every visitor's browser
- What's wrong
- The key that skips every database rule is stored in a setting whose name starts with VITE_. Anything with that prefix is copied into the code every visitor's browser downloads.
- Why it matters
- Anyone who views your page source can copy the key and read, change or delete every row in your database, without even signing up.
- How to fix it
- Create a new service key in Supabase, keep it only on the server, and delete the old one.
- Where
.env (VITE_SUPABASE_SERVICE_KEY), used in src/lib/admin.ts line 6createClient(url, import.meta.env.VITE_SUPABASE_SERVICE_KEY)
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
My app uses the Supabase service key in browser code through VITE_SUPABASE_SERVICE_KEY in src/lib/admin.ts. Move every call that needs that key into a server function, read the key there from an environment variable without the VITE_ prefix, and remove VITE_SUPABASE_SERVICE_KEY from the project.
- How to check it worked
- Open your live site, view the page source and search for "service_role". Nothing should be found.
CriticalYour OpenAI key is in the code every visitor downloads
- What's wrong
- The app calls the AI straight from the browser, so the AI key ships inside the page.
- Why it matters
- Anyone can copy the key and run their own AI work on your account. You get the bill.
- How to fix it
- Create a new key, call the AI only from a server function, and delete the old key.
- Where
src/lib/ai.ts line 3const openai = new OpenAI({ apiKey: import.meta.env.VITE_OPENAI_API_KEY, dangerouslyAllowBrowser: true })
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
Move every OpenAI call out of src/lib/ai.ts into a server function that reads OPENAI_API_KEY (no VITE_ prefix). Remove dangerouslyAllowBrowser and the VITE_OPENAI_API_KEY setting.
- How to check it worked
- View your live site's page source and search for "sk-". Nothing should be found.
2. Who can reach whose data
3 problems
CriticalAny signed-in member can read every other member's records
- What's wrong
- The app asks the database for the members list straight from the browser, and the members table has no rules saying who may read which rows. So the database answers with everyone.
- Why it matters
- One member who opens the browser's developer tools can download every member's name, email and uploaded files. That is your customers' data, and a breach notice to all of them.
- How to fix it
- Turn on row level security for the members table and add a rule that each member reads only rows where user_id matches their own.
- Where
src/lib/supabase.ts line 14const { data } = await supabase.from('members').select('*')
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In Supabase, turn on row level security for the members table. Add a policy so a signed-in user can select only the rows where user_id equals auth.uid(). Then change src/lib/supabase.ts line 14 so it asks only for the signed-in member's own row.
- How to check it worked
- Sign in as one test member and try to load another member's row. The database should return nothing.
CriticalAnyone signed in can open any order by changing the number in the address
- What's wrong
- The order page loads whatever order number is in the address and never checks it belongs to the person asking.
- Why it matters
- A member who changes /orders/1041 to /orders/1040 sees someone else's name, address and what they bought, and can keep going through every order.
- How to fix it
- Load the order only if it belongs to the signed-in member.
- Where
api/orders/[id].ts line 8const order = await db.from('orders').select('*').eq('id', req.query.id).single()
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In api/orders/[id].ts, add .eq('member_id', <the signed-in user's id from the session>) to the query, and return 404 when no row comes back.
- How to check it worked
- Sign in as one test member and open another member's order number. It should say not found.
HighThe admin page checks the role in the browser, not on the server
- What's wrong
- The admin page hides itself unless the browser says you are an admin. The data behind the page never checks, so it answers anyone who is signed in.
- Why it matters
- A member who edits one value in the browser sees every order and refund.
- How to fix it
- Check the admin role inside the server function that returns the data, and refuse the request when it is missing.
- Where
src/pages/Admin.tsx line 22if (user.role !== 'admin') return <Navigate to='/' />
- Found by
- One of our two AI readers
- Paste this into your AI tool
The admin data is protected only in src/pages/Admin.tsx. Add a check in the server function that returns the admin data: look up the signed-in user's role on the server, and return a 403 error unless it is admin. Keep the browser check as well.
- How to check it worked
- Sign in as an ordinary member and call the admin data directly. It should refuse with a 403.
3. AI features
3 problems
CriticalThe chat assistant will hand any member another member's records
- What's wrong
- Before answering, the chat loads the whole members table into the AI's instructions, then trusts the AI to only talk about the person asking.
- Why it matters
- A member who types "list every member's email and what they paid" gets the answer. The AI is not a lock, and anyone can talk it past its instructions.
- How to fix it
- Give the chat only the signed-in member's own rows, chosen on the server.
- Where
api/chat.ts line 18const context = await db.from('members').select('*')
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In api/chat.ts, stop loading the whole members table into the prompt. Load only the rows where user_id equals the signed-in user's id, taken from their session on the server, and pass only those to the AI.
- How to check it worked
- Sign in as one test member and ask the chat for another member's email. It should not know it.
HighAnyone can run up your AI bill with no limit
- What's wrong
- The chat feature sends every message to the AI with no limit per person, per day or per month.
- Why it matters
- One script left running overnight could spend thousands of dollars on your AI account before you notice.
- How to fix it
- Add a daily limit per signed-in user, and set a hard monthly spend cap in your AI provider's dashboard.
- Where
api/chat.ts line 31const reply = await openai.chat.completions.create({ model, messages })
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In api/chat.ts, before calling the AI, count how many chat messages the signed-in user has sent today and refuse with a friendly message once they reach 50. Store the count in the database, not in the browser.
- How to check it worked
- Send 51 messages as one test user. The 51st should be refused.
HighThe chat can promise discounts and refunds, and the app honours them
- What's wrong
- When the AI's answer contains a discount code, the app applies it. Nothing checks the code is real.
- Why it matters
- A member who talks the AI into it gets your program at any price they like.
- How to fix it
- Never act on the AI's words. Discounts come only from codes your server created.
- Where
api/chat.ts line 44if (reply.includes('DISCOUNT:')) await applyDiscount(user, code)
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In api/chat.ts, remove the branch that applies a discount from the AI's reply. Discounts should only be applied at checkout from codes stored in the database.
- How to check it worked
- Ask the chat for a 100% discount code and try it at checkout. It should be refused.
4. Sign-in and accounts
3 problems
CriticalAnyone can sign up as an admin
- What's wrong
- The sign-up function saves every field it is sent, including the role. Add role: admin to the form and you are an admin.
- Why it matters
- A stranger can create their own admin account in a minute and see and change everything.
- How to fix it
- Save only the fields sign-up needs, and set the role on the server.
- Where
api/signup.ts line 15await db.from('members').insert({ ...req.body })
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In api/signup.ts, stop spreading req.body into the insert. Save only name, email and password hash, and always set role to 'member' on the server.
- How to check it worked
- Sign up with an extra field role=admin. The new account should be a member.
HighA password reset link works forever, and more than once
- What's wrong
- The reset link has no expiry and is not cancelled after it is used.
- Why it matters
- Anyone who ever sees an old reset email, in a shared inbox or a forwarded message, can take over that account months later.
- How to fix it
- Make reset links expire after one hour and stop working once used.
- Where
api/reset-password.ts line 27const row = await db.from('resets').select().eq('token', token).single()
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In api/reset-password.ts, refuse a reset token that is older than one hour or has already been used, and mark the token used as soon as the password changes.
- How to check it worked
- Use a reset link once, then open it again. The second time should be refused.
HighNothing stops someone guessing passwords all day
- What's wrong
- The sign-in form accepts unlimited attempts from anyone.
- Why it matters
- A script can try thousands of common passwords against your members' emails until one works.
- How to fix it
- Limit failed sign-ins per email and per connection, and slow down after five.
- Where
api/login.ts line 10
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In api/login.ts, count failed sign-ins per email and per IP address in the database. After 5 failures in 15 minutes, refuse further attempts for 15 minutes.
- How to check it worked
- Get the password wrong 6 times as a test member. The sixth should be refused.
5. Privacy and regulated data
3 problems
CriticalMembers' uploaded files sit in a public folder
- What's wrong
- The storage folder for members' files is set to public. Every file has a web address that works without signing in.
- Why it matters
- Contracts, ID scans and anything else your members upload can be opened by anyone who has or guesses the address, and search engines can find them.
- How to fix it
- Make the folder private and hand out short-lived links only to the member who owns each file.
- Where
supabase/migrations/0003_storage.sql line 4insert into storage.buckets (id, public) values ('files', true)
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
Change the files bucket to private. Add a storage policy so a signed-in user can read only files under a folder named with their own user id, and change the app to show files through signed links that expire after 10 minutes.
- How to check it worked
- Copy the address of an uploaded file, sign out, and open it. It should be refused.
CriticalCard numbers are saved in your own database as plain text
- What's wrong
- Before sending a payment to Stripe, the checkout form saves the full card number, expiry date and security code in your database.
- Why it matters
- Anyone who gets into the database (see F2 and F5) leaves with your customers' cards. Storing them at all breaks the card networks' rules.
- How to fix it
- Stop saving card details anywhere. Let Stripe's own form collect them, and delete what is stored.
- Where
src/pages/Checkout.tsx line 51await supabase.from('payments').insert({ card_number, expiry, cvc })
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
Remove the payments insert from src/pages/Checkout.tsx and collect cards only with Stripe Elements or Stripe Checkout. Then write a migration that deletes the card_number, expiry and cvc columns.
- How to check it worked
- Make a test payment and look in the payments table. No card number should be there.
MediumA deleted member's details stay in the database forever
- What's wrong
- Deleting an account only hides it. The name, email and files stay.
- Why it matters
- A member who asks you to delete their data has not had it deleted, which privacy laws require.
- How to fix it
- Delete or anonymise a member's personal details when they delete their account.
- Where
api/delete-account.ts line 6
- Found by
- One of our two AI readers
6. Untrusted input
2 problems
HighUploads accept any file of any size
- What's wrong
- The upload button sends whatever file it is given straight to storage. Nothing checks the type or the size, in the browser or on the server.
- Why it matters
- Someone can upload a web page full of harmful code, or a 5 GB file, under your app's name. You pay for the storage and your domain gets flagged.
- How to fix it
- Limit uploads to the file types you need and a maximum size, and enforce both in the storage rules.
- Where
src/components/Upload.tsx line 40await supabase.storage.from('files').upload(path, file)
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
Add a storage policy for the files bucket that accepts only image/png, image/jpeg and application/pdf up to 10 MB. Add the same check in src/components/Upload.tsx so members see a clear message before uploading.
- How to check it worked
- Try to upload a 20 MB file and an .html file as a test member. Both should be refused.
HighThe chat shows the AI's answer as live web code
- What's wrong
- Whatever the AI answers is put on the page as working web code, not as text.
- Why it matters
- Someone who gets the AI to repeat a line of script can run it in another member's browser and act as them.
- How to fix it
- Show the AI's answer as plain text, or clean it before showing it.
- Where
src/components/ChatBubble.tsx line 12<div dangerouslySetInnerHTML={{ __html: reply }} />
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In src/components/ChatBubble.tsx, stop using dangerouslySetInnerHTML. Render the reply as text, or pass it through DOMPurify first.
- How to check it worked
- Ask the chat to repeat <img src=x onerror=alert(1)>. Nothing should pop up.
7. Supply chain and repo control
2 problems
HighYour app runs a version of a library with a published break-in
- What's wrong
- The lockfile pins an old version of the library that reads uploaded images. That version has a publicly known flaw that lets a crafted file run code on your server.
- Why it matters
- The instructions for using it are public. Automated scanners look for exactly this.
- How to fix it
- Update the library to the fixed version and turn on GitHub's automatic security updates.
- Where
package-lock.json
- Found by
- One of our two AI readers
- Paste this into your AI tool
Update the image library in package.json to its latest version, run the install so package-lock.json changes with it, and check the upload and resize features still work.
- How to check it worked
- Run npm audit. It should report no high or critical problems.
HighTwo former contractors can still publish straight to your live app
- What's wrong
- Two people who worked on the app last year still have admin rights to the repository, and anything pushed goes live with no review.
- Why it matters
- One stolen contractor password puts any code its holder likes on your live site.
- How to fix it
- Remove anyone who no longer works on the app, and require a review before changes go live.
- Where
GitHub repository settings, as listed in the history
- Found by
- One of our two AI readers
- Paste this into your AI tool
List the people with access to this GitHub repository and remove anyone who no longer works on the app. Then add a rule that changes to main need one approval.
- How to check it worked
- Open Settings, Collaborators in GitHub. Only current people should be listed.
8. Wrong results
2 problems
HighThe price is worked out in the browser, so a buyer can change it
- What's wrong
- The checkout page adds up the total itself and tells the server how much to charge. The server charges whatever it is told.
- Why it matters
- Anyone who edits one number in the browser buys your $497 program for $1.
- How to fix it
- Have the server look up the price of what is being bought and ignore any amount sent from the browser.
- Where
src/pages/Checkout.tsx line 63body: JSON.stringify({ amount: total })
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In the server function that creates the Stripe payment, stop reading amount from the request. Look up the price of the product id on the server, apply any discount there, and charge that.
- How to check it worked
- Change the amount in the browser before paying as a test buyer. The charge should still be the full price.
HighAnyone can mark an order as paid
- What's wrong
- The payment webhook believes any message sent to it. It never checks that the message came from Stripe.
- Why it matters
- Someone who sends a fake "payment succeeded" message gets your program without paying.
- How to fix it
- Check Stripe's signature on every webhook message and refuse any that fail.
- Where
api/stripe-webhook.ts line 9const event = JSON.parse(req.body)
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In api/stripe-webhook.ts, verify each event with stripe.webhooks.constructEvent using the raw body and STRIPE_WEBHOOK_SECRET, and return 400 if it fails.
- How to check it worked
- Send a made-up payment message to the webhook. It should be refused with 400.
9. Breaks in use
2 problems
HighThe app stops working when about 60 people use it at once
- What's wrong
- Every request opens its own database connection and never closes it. The database allows 60.
- Why it matters
- The day your launch email goes out, the app goes down for everyone, at the moment the most people are looking.
- How to fix it
- Use one shared connection pool and close connections when each request ends.
- Where
api/_db.ts line 9const client = new Client(process.env.DATABASE_URL)
- Found by
- One of our two AI readers
- Paste this into your AI tool
In api/_db.ts, replace the new Client per request with a single shared pool using the database's pooled connection address, and make sure every request releases its connection, including when it fails.
- How to check it worked
- Send 100 requests at once to the app. All of them should get an answer.
HighUploads over 4.5 MB fail without telling anyone
- What's wrong
- Files go through a server function, and your host refuses anything over 4.5 MB. The member sees a spinner that never stops.
- Why it matters
- Members with real documents cannot use the feature, and you never hear why.
- How to fix it
- Upload large files straight to storage, and show an error when an upload fails.
- Where
src/components/Upload.tsx line 40
- Found by
- One of our two AI readers
- Paste this into your AI tool
Change src/components/Upload.tsx to upload files directly to Supabase Storage with a signed upload URL instead of through the server function, and show a clear message if the upload fails.
- How to check it worked
- Upload a 10 MB PDF as a test member. It should arrive.
10. Cost and scale
2 problems
HighEvery page visit downloads every member's records
- What's wrong
- The dashboard loads the whole members table each time anyone opens it, then shows one row.
- Why it matters
- Your database bill and page load time grow with every new member, and every visit hands out everyone's data.
- How to fix it
- Load only the rows the page shows.
- Where
src/pages/Dashboard.tsx line 19const { data } = await supabase.from('members').select('*')
- Found by
- Both AI readers, working separately
- Paste this into your AI tool
In src/pages/Dashboard.tsx, load only the signed-in member's own row, and page through lists 50 rows at a time.
- How to check it worked
- Open the dashboard with the browser's network tab open. One row should come back.
MediumYour database plan runs out at about 2,000 members
- What's wrong
- At the current size per member, the free database plan fills at about 2,000 members. The app then stops saving.
- Why it matters
- Your best month of sign-ups is the month the app stops working.
- How to fix it
- Set an alert at 80% of the plan and move to a paid plan before then.
- Where
supabase/config.toml
- Found by
- One of our two AI readers
11. Logging and monitoring
2 problems
MediumErrors are never recorded anywhere you would see them
- What's wrong
- When something fails on the server, the app does not write it down or tell anyone.
- Why it matters
- You hear about failures from customers, if at all, and you cannot tell how often they happen.
- How to fix it
- Add an error tracker and have it email you when a new error appears.
- Where
api/
- Found by
- One of our two AI readers
MediumThere is no record of who changed or deleted what
- What's wrong
- When an admin changes an order or deletes a member, nothing writes down who did it or when.
- Why it matters
- After a mistake or a break-in you cannot tell what happened.
- How to fix it
- Keep a simple log of every admin change: who, what and when.
- Where
api/
- Found by
- One of our two AI readers
12. Layer separation
1 problem
MediumThe database is called from 38 different browser files
- What's wrong
- Every page talks to the database on its own, so the same rule is written over and over.
- Why it matters
- Fix a security rule in one place and the other 37 still have the hole.
- How to fix it
- Put database calls in one shared set of server functions.
- Where
src/
- Found by
- One of our two AI readers
13. Tests and releases
2 problems
MediumNothing tests the payment code, and every change goes straight live
- What's wrong
- There are no tests for checkout or refunds, and changes are published to the live site the moment they are saved.
- Why it matters
- The next time your AI tool "fixes" something nearby, payments can break and nobody knows until a customer complains.
- How to fix it
- Add a test that takes a test payment, and have changes go to a preview site before the live one.
- Where
.github/ (none)
- Found by
- One of our two AI readers
MediumA database change file was edited after it had already run
- What's wrong
- The file that set up the members table was changed after it was applied, so your live database and your code no longer match.
- Why it matters
- The next fresh setup, or a restore, builds a different database from the one you run.
- How to fix it
- Never edit an applied change file. Add a new one with the difference.
- Where
supabase/migrations/0004_members.sql
- Found by
- One of our two AI readers
14. Recovering from mistakes
2 problems
MediumDeleting a member also deletes their orders, with no undo
- What's wrong
- Removing a member's account wipes every order they ever placed, and there is no way to bring them back from inside the app.
- Why it matters
- One wrong click loses payment records you need for refunds and tax.
- How to fix it
- Mark members as removed instead of deleting them, and keep their orders.
- Where
supabase/migrations/0002_orders.sql line 7member_id uuid references members(id) on delete cascade
- Found by
- One of our two AI readers
LowRe-running a failed member import adds everyone twice
- What's wrong
- The import has no check for members already added.
- Why it matters
- One retry doubles your list and every email you send.
- How to fix it
- Skip members whose email is already in the table.
- Where
scripts/import-members.ts
- Found by
- One of our two AI readers
15. What users see (apps with a screen)
2 problems
LowThe sign-up form shows 'Error 400' instead of what went wrong
- What's wrong
- When sign-up fails, the form shows a code number instead of the reason.
- Why it matters
- A customer who mistypes their email sees a code, doesn't know what to do, and gives up.
- How to fix it
- Show the message the server sends back, in plain words.
- Where
src/pages/Signup.tsx line 58setError(`Error ${res.status}`)
- Found by
- One of our two AI readers
LowThe sign-up form cannot be used with a keyboard alone
- What's wrong
- The submit button is a picture with a click handler, so it cannot be reached with the Tab key.
- Why it matters
- Some customers cannot sign up at all.
- How to fix it
- Use a real button.
- Where
src/pages/Signup.tsx
- Found by
- One of our two AI readers
16. Requirements docs
Checked, nothing found
We checked this area and found nothing to fix. What we checked: the README says what each part of the app is for.
17. Licences and ownership
Checked, nothing found
We checked this area and found nothing to fix. What we checked: every dependency's licence allows selling the product.
18. Handover risk
Checked, nothing found
We checked this area and found nothing to fix. What we checked: a new developer can run the app from the README.
19. Operations
Not checked
Backups live in your database dashboard, which reading the code cannot see. Check it under Database, Backups.
How this was done
- Your code was read only. It was never changed and never run.
- It was read by two AI readers (Anthropic Claude) working separately, neither seeing the other's notes, each in two passes: security first, then code quality and the rest of the areas above. Each problem says whether one reader or both found it.
- Every high and critical problem was checked automatically against the exact line it quotes.
- An attacker pass then worked out how the problems could be combined, reading the code that connects them.
- Each fix prompt was written after re-reading the file it changes.
- A person at Impossible Means Go reviewed this report before it was sent to you. Nobody at our company opened your code.
- The copy of your code we read was deleted at 26 October 2026, 10:55 Manila time (UTC+8).
- Reply to the email this report came with to have it removed sooner than its expiry.
What to do next
- Start with the findings marked critical, then high. Each one says what to change in a single line. Paste the finding, the file and the line into your AI tool and ask it to make that change.
- Change any password or key this report names. Deleting it from the code is not enough: it is still in your project's history, and it still works until you replace it with a new one.
- Check the settings this report marks "depends on". We could not see them from the code. The report names each setting and where to find it.
- Save a copy. This link stops working on 25 November 2026, 30 days after it was sent.
Your code was deleted from our systems at 26 October 2026, 10:55 Manila time (UTC+8).
This report shows what we found by reading your code on 26 October 2026. It is not proof that your app is secure, and it does not cover changes made after that date.
Keep it checked as you build
The findings above are what was in your code on 26 October 2026. Every change you make from here adds new code. When you have made a round of changes, you can order another audit of the same repo here: https://www.impossiblemeansgo.com
Build the next one right from the first day
Most of what is in this report came from shortcuts the AI tool took while you were building fast. Fixing them fixes this app. Knowing how to set up the next one so the shortcuts never get in is a different skill, and it is what Impossible Means Go teaches.
Impossible Means Go is run by Atiba de Souza. Business owners come to build real AI in their own business, in the room with him, and leave with it working.
If you want to talk about what you would build next, reply to the email this report came with.