Skip to content
BuildThenTestedSee packages

Sample report

This is the exact format you receive: a summary for a non-technical reader, a findings table, one page per finding with mechanism, path, observed output and fix direction, what we did not find, and next steps. The app is fictional and every page says so.

Synthetic: Loamly does not exist. Format of the Pre-launch check ($490, 48 hours).

Sample report · synthetic, fictional appPage 1 of 11

Loamly: Pre-launch check

Prepared for
Loamly (fictional)
Prepared by
BuildThenTested
Tested on
2026-09-14
Delivered on
2026-09-16
Built with
Lovable
Stack
Supabase, Stripe, Vercel

A fictional plant-care subscription app: users log plants, get watering reminders, and pay a monthly plan through Stripe. Built with Lovable on Supabase, deployed on Vercel. Every name, path and finding in this report is invented to show the report format; the app does not exist.

Findings describe what we observed on the version you gave us access to, on the date of testing. Changes after that date are not covered until re-checked.

Sample report · synthetic, fictional appPage 2 of 11

Summary for a non-technical reader

We tested Loamly the way a first wave of users and an opportunistic visitor would, using two test accounts and the public parts of the app. We found seven things worth fixing before launch: two critical, three high, two medium.

The two critical findings both let one user see or change another user's data. One is a database table with no access rules, so anyone with the app's public key can read every plant record. The other is a plan check that runs only in the browser, so a free user can reach paid features by editing one value.

Both critical fixes are small: turn on row-level rules for the plants table and move the plan check to the server. We describe the exact change for each finding and will re-test within 14 days of your fixes at no extra cost.

Nothing in this report means the app is finished being tested. It lists what we found on the version we tested, on the date we tested it, against the pre-launch checklist.

  • Critical 2
  • High 3
  • Medium 2
Sample report · synthetic, fictional appPage 3 of 11

Findings by severity

Findings by severity
IDSeverityFindingAffected path
F-01CriticalRow-level rules are off on the plants tableSupabase table `public.plants`; REST endpoint `/rest/v1/plants`
F-02CriticalPaid plan enforced only in the browserComponent `PremiumReminders.tsx`; Supabase Edge Function `send-reminders`
F-03HighStripe webhook accepts unsigned eventsEdge Function `stripe-webhook`
F-04HighAdmin page reachable by URL for any signed-in userRoute `/admin`; table `profiles`
F-05HighNo rate limit on password resetSupabase Auth `/auth/v1/recover` via the app's reset form
F-06MediumSecurity headers missing on the production siteAll responses from `loamly.example`
F-07MediumSpinner never stops when the reminders call failsComponent `PremiumReminders.tsx`, function `handleSchedule`

Scope tested

  • Secrets and keys: JavaScript bundle, public API responses, error messages
  • Data access: every Supabase table the app touches, with the anon key and two signed-in roles
  • Login and permissions: routes, roles, admin area, password reset
  • Reliability: error handling, rate limits on public endpoints
  • Deploy and config: headers, debug settings, environment variables reaching the browser
Sample report · synthetic, fictional appPage 4 of 11

F-01: Row-level rules are off on the plants table

Mechanism
The plants table was created without row-level security (RLS: per-row rules deciding who may read or change a row). The app's public key, which every visitor's browser holds, can therefore read and modify every row in the table.
Affected path
Supabase table `public.plants`; REST endpoint `/rest/v1/plants`
What we observed
GET /rest/v1/plants?select=id,user_id,nickname,notes  (anon key, not signed in)
HTTP 200
[{"id":1,"user_id":"7c1e...","nickname":"Fern in the kitchen","notes":"Gift from mum"}, ... 1,284 rows]
Fix direction
Enable RLS on plants and add two policies: users may select rows where user_id = auth.uid(), and users may insert, update or delete rows where user_id = auth.uid(). Then repeat the request above; it should return an empty array.
Re-check
We will repeat the anon and cross-user requests and confirm both return no rows.
Sample report · synthetic, fictional appPage 5 of 11

F-02: Paid plan enforced only in the browser

Mechanism
The premium reminder feature checks profile.plan === 'pro' in the React component and then calls the send-reminders function. The function does not check the plan itself, so a free user can call it directly.
Affected path
Component `PremiumReminders.tsx`; Supabase Edge Function `send-reminders`
What we observed
POST /functions/v1/send-reminders  (free test account, valid session)
HTTP 200 {"scheduled": 12}
Fix direction
Read the plan from the subscriptions table inside the function using the caller's user id, and return HTTP 403 when the plan is not pro. Keep the browser check for the user interface only.
Re-check
We will call the function with the free account and expect HTTP 403.
Sample report · synthetic, fictional appPage 6 of 11

F-03: Stripe webhook accepts unsigned events

Mechanism
The webhook handler parses the request body as JSON and updates subscriptions without calling Stripe's signature check, so anyone who knows the URL can post a fake checkout.session.completed event.
Affected path
Edge Function `stripe-webhook`
What we observed
POST /functions/v1/stripe-webhook  (no Stripe-Signature header, hand-written event)
HTTP 200; subscriptions.plan for test user changed to 'pro'
Fix direction
Verify the Stripe-Signature header with your webhook signing secret before reading the event, and reject requests that fail with HTTP 400. Stripe's docs show the exact call for every SDK.
Re-check
We will replay the unsigned event and expect HTTP 400 with no change to the table.
Sample report · synthetic, fictional appPage 7 of 11

F-04: Admin page reachable by URL for any signed-in user

Mechanism
The /admin route is hidden from the menu for non-admins but has no server-side role check. A signed-in free user who types the URL sees the user list. Data on the page is limited by RLS on profiles, so names and emails are visible but not editable.
Affected path
Route `/admin`; table `profiles`
What we observed
GET /admin  (free test account)
HTTP 200, page renders 1,284 profile rows (name, email, plan)
Fix direction
Add a server-side check that the caller's role is admin before the page loads, and add an RLS policy so profiles returns only the caller's own row unless the caller is an admin.
Re-check
We will open `/admin` with the free account and expect a redirect or HTTP 403, and query `profiles` expecting one row.
Sample report · synthetic, fictional appPage 8 of 11

F-05: No rate limit on password reset

Mechanism
The password reset endpoint accepted a burst of requests for the same address within one minute without slowing down or blocking. This allows email flooding of any user and makes reset-token guessing cheaper.
Affected path
Supabase Auth `/auth/v1/recover` via the app's reset form
What we observed
300 POST requests in 60 s from one IP: 300 x HTTP 200
Fix direction
Enable Supabase Auth rate limits for email sending in the project dashboard and set a per-IP limit on the reset form at the edge (Vercel Firewall or middleware).
Re-check
We will repeat the burst and expect HTTP 429 after the configured limit.
Sample report · synthetic, fictional appPage 9 of 11

F-06: Security headers missing on the production site

Mechanism
Responses carry no Content-Security-Policy, X-Frame-Options or Strict-Transport-Security headers, so the site can be framed by another site and browsers do not enforce HTTPS-only connections.
Affected path
All responses from `loamly.example`
What we observed
curl -sI https://loamly.example/ | grep -iE 'content-security|frame-options|strict-transport'
(no output)
Fix direction
Add the headers in next.config (or vercel.json) starting with X-Frame-Options: DENY and Strict-Transport-Security, then introduce a Content-Security-Policy in report-only mode and tighten it.
Re-check
We will fetch the homepage headers and confirm the three are present.
Sample report · synthetic, fictional appPage 10 of 11

F-07: Spinner never stops when the reminders call fails

Mechanism
When send-reminders returns an error, the catch block logs to the console and never resets the loading state, so the user sees an endless spinner and often retries, scheduling duplicate reminders.
Affected path
Component `PremiumReminders.tsx`, function `handleSchedule`
What we observed
Blocked the function URL in devtools, clicked Schedule: spinner persisted 5 minutes; console shows 'TypeError: Failed to fetch'
Fix direction
Reset the loading state in a finally block and show a short message with a retry button. Make the function idempotent per user and day so a retry cannot double-schedule.
Re-check
We will repeat the blocked call and expect a visible error state within a few seconds.
Sample report · synthetic, fictional appPage 11 of 11

Tested and not found, and next steps

Checked and not found on the tested version

  • No Supabase service-role key or Stripe secret key in the JavaScript bundle or in any response we could reach
  • No secrets in the public GitHub repository history that was shared with us
  • Password reset tokens expired after use
  • Debug mode off in production; environment variables reaching the browser were limited to the public Supabase URL and anon key
  • Payment amounts are taken from the server-side price id, not from the browser

Next steps

  1. Fix F-01 and F-02 first; both are small changes in the Supabase dashboard and one function
  2. Fix F-03 before enabling live payments
  3. Tell us when the fixes are deployed; we re-test the critical and high findings within 14 days, included in the package
  4. Consider the ready-to-charge check before switching Stripe to live mode

We test against a defined checklist and report what we find. We do not guarantee that an app is free of vulnerabilities. We are not a certified penetration-testing firm and do not issue compliance attestations. We do not rebuild your app; fixes are yours or your developer's, and we re-check them.

Questions about the report

what does a vibe code audit report look like

Like the pages above. It opens with a one-page summary a non-technical reader can act on, then a findings table by severity. Each finding gets a page with the mechanism, the affected path, what we observed and the fix direction. It closes with what we tested and did not find, and next steps.

is the sample report from a real client

No. The app, the names, the paths and the findings are invented to show the format. We do not publish client reports. The patterns behind each finding are real and link to the catalogue.

do i get the report as a pdf

Yes. Every package delivers a PDF and a link to the same content. The PDF of this sample is generated from the same data as this page.

Order the Pre-launch check, compare all packages, or read the catalogue behind the findings.