AI App CheckA QA lead tests your AI-built app before launch

Sample Audit Report: DemoNotes

Full Audit · demo app · tested October 29, 2026

App
DemoNotes (demo app built by AI App Check)
URL audited
A local copy of the app (not deployed)
Tier
Full Audit ($499)
Audit date
October 29, 2026
Sign-off
Reviewed and signed off by our QA lead

Verdict

Not ready to launch.

8 findings. The critical ones let anyone see customer data and pay $0.01 instead of $19.

4 × S1 critical2 × S2 major2 × S3 minor

Fix these first

  1. Your Stripe secret key is visible to every visitor (F-01). Anyone can copy it from the page code and use your Stripe account. Move it to the server and rotate it.
  2. Customer names, emails and phone numbers can be read by anyone (F-02, F-03). The profiles table and the orders API answer without a login.
  3. Shoppers can set their own price (F-04). Changing one number in the browser made the $19 plan cost $0.01 at checkout.

How we rate problems

SeverityMeans
S1 criticalData exposed, login can be bypassed, or payments broken. Fix before launch.
S2 majorA core flow is broken or unsafe. Fix before launch if you can.
S3 minorCosmetic or hardening. Fix soon after launch.

Scope

Findings

IDSeverityTitleWhere
F-01S1Stripe secret key visible in the page code/static/config.js, loaded on every page
F-02S1Customer profiles readable by anyonedatabase table profiles
F-03S1Orders list works without logging in/api/orders
F-04S1Price can be changed in the browserPricing page → "Upgrade to Pro"
F-05S2Script error on the pricing page/pricing
F-06S2No limit on wrong-password attemptslogin (/api/login)
F-07S3Security headers missingevery page
F-08S3"About us" link is brokentop menu, every page

In a paid audit, every finding comes with a fix prompt you can paste into Lovable, Bolt, Replit or Cursor. This sample shows the first one in full (F-01); the rest are kept for clients.

S1 F-01 · Stripe secret key visible in the page code

Where: /static/config.js, loaded on every page

What happens: the file sent to every visitor's browser contains a Stripe secret key (sk_test_…).

What should happen: secret keys stay on the server; the browser only ever gets the publishable pk_ key.

Why it matters: anyone who opens the page code can use the key to read customers and charges, or issue refunds, in your Stripe account.

Steps to reproduce:

  1. Open any page and view the page source.
  2. Open /static/config.js and search for sk_.

Evidence: Playwright trace, test "no secret keys in the browser bundle" (key redacted to its first 12 characters). Found by security.spec.ts.

Fix prompt (paste into your AI builder)
Please fix this problem in my app.

PROBLEM: My Stripe secret key is inside a file every visitor's browser downloads (/static/config.js, the "stripeSecretKey" setting).
IT SHOULD: Keep the Stripe secret key only on the server. The browser should only ever get the publishable key that starts with pk_.
WHERE: /static/config.js and any code that calls Stripe.
DO NOT: change the design, the prices, or how checkout looks.
SECURITY RULES: never put secret keys in front-end code; keep them in server-side environment variables, and call Stripe only from a server function.
WHEN DONE: list the files you changed, explain the fix in one sentence, and remind me to rotate (replace) the old secret key in my Stripe dashboard.
I WILL CHECK IT BY: opening my site, viewing the page source and /static/config.js, and searching for "sk_". It should find nothing.

S1 F-02 · Customer profiles readable by anyone

Where: database table profiles

What happens: using only the public key that ships with the app, a logged-out visitor can read rows from profiles (columns: id, email, full_name, phone).

What should happen: each user can read only their own profile; logged-out visitors get nothing.

Why it matters: anyone can download your customers' names, emails and phone numbers.

Steps to reproduce:

  1. Take the project URL and public (anon) key from the app's page code.
  2. Request one row from profiles without logging in.

Evidence: output of our read-only database check (column names only; no row values recorded). The notes table returned no rows, so it is protected. Found by supabase-rls-check.ts.

Fix prompt included in your audit report. Every finding in a paid audit comes with a paste-ready prompt for Lovable, Bolt, Replit or Cursor. F-01 above shows what one looks like.

S1 F-03 · Orders list works without logging in

Where: /api/orders

What happens: a request with no login gets status 200 and a list of every order, with customer emails and totals.

What should happen: the route returns 401 when nobody is logged in, and only the logged-in user's own orders otherwise. (/api/me already does this correctly.)

Why it matters: anyone can see every customer's orders.

Steps to reproduce:

  1. Open a private browser window (not logged in).
  2. Go to /api/orders.

Evidence: Playwright trace, test "private API routes reject logged-out requests" (reported 200 /api/orders). Found by security.spec.ts.

Fix prompt included in your audit report. Every finding in a paid audit comes with a paste-ready prompt for Lovable, Bolt, Replit or Cursor. F-01 above shows what one looks like.

S1 F-04 · Price can be changed in the browser

Where: Pricing page → "Upgrade to Pro"

What happens: the page sends the price (price_cents=1900) from the browser and the server trusts it. Changing it to 1 made the Stripe checkout page show $0.01 instead of $19.00.

What should happen: the server decides the price; anything the browser sends about price is ignored.

Why it matters: anyone can buy the Pro plan for one cent.

Steps to reproduce:

  1. Open /pricing and change the hidden price field to 1 (browser developer tools).
  2. Click "Upgrade to Pro" and read the total on the checkout page.

Evidence: Playwright trace and page snapshot, test "price cannot be changed from the browser" ("Total due $0.01"). Found by checkout.spec.ts (Stripe test mode).

Fix prompt included in your audit report. Every finding in a paid audit comes with a paste-ready prompt for Lovable, Bolt, Replit or Cursor. F-01 above shows what one looks like.

S2 F-05 · Script error on the pricing page

Where: /pricing

What happens: when the page loads, the browser reports initPricingAnalytics is not defined. The checkout button still worked in our tests.

What should happen: the page loads with no errors in the browser console.

Why it matters: a script that crashes on your money page can stop tracking, or break the page in some browsers. Rated S2 per our scale (console errors on a key page).

Steps to reproduce:

  1. Open /pricing.
  2. Open the browser console (right-click → Inspect → Console).

Evidence: Playwright trace, test "no console errors on /pricing". Home, login and signup had no errors. Found by console.spec.ts.

Fix prompt included in your audit report. Every finding in a paid audit comes with a paste-ready prompt for Lovable, Bolt, Replit or Cursor. F-01 above shows what one looks like.

S2 F-06 · No limit on wrong-password attempts

Where: login (/api/login)

What happens: 15 wrong passwords in a row for the test account were all answered with 401; none was slowed down or blocked (no 429).

What should happen: after about 5 failed tries in a minute, the app blocks further tries for a while and says so.

Why it matters: without a limit, a script can keep guessing passwords for your users' accounts.

Steps to reproduce:

  1. On the login page, enter the test email with a wrong password 15 times.
  2. Every attempt is accepted for checking; none is blocked.

Evidence: test "login is rate limited" (statuses: 401 × 15). Found by security.spec.ts (15 requests, test account only).

Fix prompt included in your audit report. Every finding in a paid audit comes with a paste-ready prompt for Lovable, Bolt, Replit or Cursor. F-01 above shows what one looks like.

S3 F-07 · Security headers missing

Where: every page

What happens: none of the 5 standard security headers is sent: Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, frame protection (X-Frame-Options or CSP frame-ancestors), Referrer-Policy.

What should happen: every page sends all 5.

Why it matters: they make some attacks much harder, like your site being loaded inside someone else's page to trick users into clicking.

Steps to reproduce:

  1. Open any page, then browser developer tools → Network.
  2. Click the page request and read its response headers.

Evidence: test "security headers and HTTPS". The http-to-https half of this test wasn't judged: the demo ran locally, without https. Found by security.spec.ts.

Fix prompt included in your audit report. Every finding in a paid audit comes with a paste-ready prompt for Lovable, Bolt, Replit or Cursor. F-01 above shows what one looks like.

S3 F-08 · "About us" link is broken

Where: top menu, every page

What happens: "About us" goes to /about-us, which returns "page not found" (404).

What should happen: the link opens an About page, or is removed.

Why it matters: visitors who click it hit a dead end, which looks unfinished.

Steps to reproduce:

  1. Click "About us" in the top menu.

Evidence: test "no broken links" (7 pages crawled, 1 broken: 404 /about-us). Found by links.spec.ts.

Fix prompt included in your audit report. Every finding in a paid audit comes with a paste-ready prompt for Lovable, Bolt, Replit or Cursor. F-01 above shows what one looks like.

Security checks

#CheckResultFinding
1Secret keys in the browser bundleFailF-01
2Exposed .env, .git or source mapsPass—
3Supabase tables open to anyoneFail profiles; notes passedF-02
4Firebase rules openNot tested app doesn't use Firebase—
5API routes without authFail /api/orders; /api/me passedF-03
6Security headers, HTTPS, cookiesFail headers. HTTPS redirect not tested (local copy). Session cookie is HttpOnly + SameSite=Lax (checked by hand; Secure flag not judged on local http)F-07
7Rate limitingFail loginF-06
—Price set by the server, not the browserFailF-04

Also passed: signup, login, wrong password rejected, logout, logged-out visitors sent to login, password reset gives the same message for unknown emails, contact form blocks an empty submit, no critical or serious accessibility issues on 4 pages, no sideways scrolling on a phone. Checkout with test cards: a payment unlocks Pro; the success page alone unlocks nothing; declined, 3D Secure and insufficient-funds cards each show an error and unlock nothing.

Your test suite

With a Full Audit you keep the Playwright suite we ran: 62 automated tests in 9 files, run on Chrome desktop and a phone screen size. Last run on the demo: 45 passed, 14 failed (the 7 automated findings × 2 sizes), 3 skipped by design. After each fix, run npx playwright test (or the included GitHub Actions workflow) and the matching test should turn green.

Limits

This is a best-effort review of one copy of the app at one point in time, not a security guarantee or a penetration test. Checkout was tested in Stripe test mode only; for this demo, Stripe's hosted page was simulated locally. Real Safari, Firebase and the password-reset email link were not tested. A retest after fixes was not done for the demo.

Want a report like this for your app?