Sample. This is a Signed Review report for a made-up app, written to show what you receive. The app, the reviewer and every finding are invented; no real app was reviewed. The evidence files describe screenshots in words instead of showing them.
Review report, order SAMPLE-0001
This report is for the app's builder. It shows how to reproduce each failure, so keep it private; a buyer or investor gets the public registry page instead.
- Review
- Signed Review
- Reviewer
- Sam Example (REV-000)
- Build reviewed
- 7c41e0b2d9a3
- Checklist
- version 1.0
- Reviewed on
- 2026-10-20
- Results
- 26 passed, 3 failed, 1 not applicable, 0 not checked
- Report fingerprint
- 95587fe3a3b733aff096fcb78d096588b718b95c919c89765cd830e59ca3fccb
Scope: Tidyhaus is a made-up cleaning-booking app, built with Lovable on Supabase with Stripe. Web app on staging.tidyhaus.example; the admin area was included.
Tested: 2026-10-15 09:30 to 2026-10-19 15:00, America/New_York. Every logged-in request was made from the test accounts named in the steps, so you can find it in your own logs.
AI assistance: Claude, to read the repository and suggest where to look first; every finding was then tested by hand from the test accounts. No keys, passwords or user data were shared with it, as the builder agreed. The reviewer ran and confirmed every result and answers for each one.
What to fix
- AR-03 (critical): Users cannot write rows as someone else or change protected fields
- SK-03 (high): Third-party keys that must sit in the browser are restricted to this app and the APIs it uses
- EL-02 (high): Passwords, tokens, card data and personal data are not written to logs
Reviewer's judgment
Customers see only their own bookings, messages and invoices, and cleaners see only the jobs assigned to them, which matches how the owner described the app. The one way around it is the role field under AR-03: any signed-in user can make themselves an admin, so fix that first. The payment flow holds up: prices come from the server, forged Stripe events are refused and repeated ones are ignored, and a paid plan cannot be switched on by hand.
Results
| Item | What it checks | Severity | Result |
|---|---|---|---|
| AR-01 | One user cannot read or change another user's records by changing an id | critical | Pass |
| AR-04 | Team or workspace members cannot see or change another team's data | critical | Not applicable |
| PM-01 | Payment webhooks are rejected unless the Stripe signature checks out | critical | Pass |
| PM-03 | A paid feature cannot be unlocked without a confirmed payment | critical | Pass |
| PM-04 | A repeated payment event does not grant credit or create an order twice | high | Pass |
| DX-01 | Data cannot be read or changed without logging in | critical | Pass |
| DX-02 | Database views follow the same access rules as their tables | critical | Pass |
| DX-03 | Responses carry only the fields the screen needs | high | Pass |
| DX-04 | Web addresses carry no keys, tokens or personal data | high | Pass |
| IN-01 | Text one user enters cannot run as code in another user's browser | critical | Pass |
| AR-02 | Ordinary users cannot reach admin pages or admin functions | critical | Pass |
| AR-03 | Users cannot write rows as someone else or change protected fields | critical | Fail |
| SK-01 | Browser code holds no service-role or other secret keys | critical | Pass |
| SK-02 | The repository and its history hold no live secrets, and leaked keys are rotated | critical | Pass |
| SK-03 | Third-party keys that must sit in the browser are restricted to this app and the APIs it uses | high | Fail |
| AU-01 | Password reset links are single-use, expire, and do not reveal which emails have accounts | high | Pass |
| AU-02 | Logging out revokes the session on the server, and any copied access token stops at its expiry | high | Pass |
| AU-03 | Sessions and access tokens expire on a defined schedule | medium | Pass |
| AU-04 | Repeated login and code attempts are rate limited | high | Pass |
| PM-02 | Prices and totals are set on the server | critical | Pass |
| BR-01 | Database backups are switched on and a restore has actually been tried | high | Pass |
| EL-01 | Error messages shown to users do not reveal internals | medium | Pass |
| EL-02 | Passwords, tokens, card data and personal data are not written to logs | high | Fail |
| EL-03 | Failed logins and refused access attempts are recorded | medium | Pass |
| TP-01 | Dependencies have no known critical or high vulnerabilities left unaddressed | high | Pass |
| TP-02 | Personal data sent to analytics, trackers or chat widgets matches the privacy policy | high | Pass |
| DX-06 | Database functions called from the browser follow the same access rules as their tables | critical | Pass |
| PD-01 | Deleting an account removes the user's records, files and sessions | high | Pass |
| PD-03 | Private uploads are not publicly reachable | critical | Pass |
| SC-01 | The app publishes a working way to report security problems | medium | Pass |
AR-01 · One user cannot read or change another user's records by changing an id
Pass
Steps to reproduce
- Signed in as test user A and opened booking 8812, saving the request that loads it.
- Signed in as test user B and sent the same GET and PATCH for booking 8812 with B's token.
- Repeated for profiles, bookings, messages and invoices, and for the bookings list with its user filter removed.
Evidence
- request and response: evidence/ar-01-request.txt
AR-04 · Team or workspace members cannot see or change another team's data
Not applicable
Steps to reproduce
- Asked the owner whether the app has teams or workspaces: it does not. Each account belongs to one person.
- Checked the schema: no table carries a team or organisation id.
Evidence
- request and response: evidence/ar-04-request.txt
- settings screenshot: evidence/ar-04-settings.txt
Reviewer's note: Tidyhaus has no teams or workspaces: each customer and each cleaner has their own account.
PM-01 · Payment webhooks are rejected unless the Stripe signature checks out
Pass
Steps to reproduce
- Read supabase/functions/stripe-webhook/index.ts.
- Sent an unsigned checkout.session.completed event to the staging webhook URL.
- Checked the bookings table for a new paid booking.
Evidence
- request and response: evidence/pm-01-request.txt
- screenshot: evidence/pm-01-screenshot.txt
- code excerpt: evidence/pm-01-code.txt
PM-03 · A paid feature cannot be unlocked without a confirmed payment
Pass
Steps to reproduce
- As free test user C, opened /checkout/success?session_id=cs_test_made_up directly.
- Sent PATCH /rest/v1/profiles setting plan to 'pro' with C's token.
- Read where the plan is set in code.
Evidence
- screenshot: evidence/pm-03-screenshot.txt
- request and response: evidence/pm-03-request.txt
- code excerpt: evidence/pm-03-code.txt
PM-04 · A repeated payment event does not grant credit or create an order twice
Pass
Steps to reproduce
- Completed one test purchase on staging.
- Resent the same event from the Stripe dashboard (Developers > Events > Resend).
- Counted bookings and confirmation emails for that session.
Evidence
- screenshot: evidence/pm-04-screenshot.txt
- code excerpt: evidence/pm-04-code.txt
DX-01 · Data cannot be read or changed without logging in
Pass
Steps to reproduce
- Signed out and recorded every data request the app makes.
- Repeated each one with no token, and queried every table in the public schema with the anon key.
Evidence
- request and response: evidence/dx-01-request.txt
- screenshot: evidence/dx-01-screenshot.txt
DX-02 · Database views follow the same access rules as their tables
Pass
Steps to reproduce
- Listed views in the public schema: one view, booking_summary.
- Queried it signed out and as user B.
Evidence
- request and response: evidence/dx-02-request.txt
DX-03 · Responses carry only the fields the screen needs
Pass
Steps to reproduce
- Compared each main screen's response fields with what the screen shows, as user B.
Evidence
- request and response: evidence/dx-03-request.txt
DX-04 · Web addresses carry no keys, tokens or personal data
Pass
Steps to reproduce
- Used sign-up, login, password reset, booking and invoice download, recording every address.
Evidence
- request and response: evidence/dx-04-request.txt
IN-01 · Text one user enters cannot run as code in another user's browser
Pass
Steps to reproduce
- Entered the checklist's test values in the booking notes, profile bio and message fields as user A.
- Viewed each as user B and inspected the rendered elements.
- Deleted the test values.
Evidence
- request and response: evidence/in-01-request.txt
- screenshot: evidence/in-01-screenshot.txt
- code excerpt: evidence/in-01-code.txt
AR-02 · Ordinary users cannot reach admin pages or admin functions
Pass
Steps to reproduce
- As the admin test account, captured the requests behind /admin/users and /admin/refunds.
- Replayed each with user B's token, and opened the admin addresses as B.
Evidence
- request and response: evidence/ar-02-request.txt
- screenshot: evidence/ar-02-screenshot.txt
- settings screenshot: evidence/ar-02-settings.txt
Reviewer's note: Passes on its own terms: an ordinary user's calls to admin functions are refused. But see AR-03, where a user can first make themselves an admin; fixing AR-03 is what makes this pass worth anything.
AR-03 · Users cannot write rows as someone else or change protected fields
Fail
Steps to reproduce
- Listed the policies on profiles: the UPDATE policy has USING (id = auth.uid()) and no column limits.
- As user B, sent PATCH /rest/v1/profiles?id=eq.[B] with {"role":"admin"}.
- Reloaded /admin/users as B.
- Expected
- The role change is refused or has no effect
- Actual
- User B became an admin and could open the admin user list
Evidence
- settings screenshot: evidence/ar-03-settings.txt
- request and response: evidence/ar-03-request.txt
SK-01 · Browser code holds no service-role or other secret keys
Pass
Steps to reproduce
- Searched every loaded JavaScript bundle and request on staging for key patterns.
- Decoded the one Supabase key found.
Evidence
- screenshot: evidence/sk-01-screenshot.txt
- file listing: evidence/sk-01-files.txt
SK-02 · The repository and its history hold no live secrets, and leaked keys are rotated
Pass
Steps to reproduce
- Ran gitleaks over all 412 commits of the repository.
- Checked .gitignore and the root config files.
Evidence
- file listing: evidence/sk-02-files.txt
- screenshot: evidence/sk-02-screenshot.txt
SK-03 · Third-party keys that must sit in the browser are restricted to this app and the APIs it uses
Fail
Steps to reproduce
- Listed third-party keys in the browser: one Google Maps key.
- Asked the owner for the key's settings screenshot.
- Expected
- Website restrictions set to the app's own addresses
- Actual
- The key has no website or API restrictions, so anyone can copy it and use it on the owner's bill
Evidence
- settings screenshot: evidence/sk-03-settings.txt
AU-01 · Password reset links are single-use, expire, and do not reveal which emails have accounts
Pass
Steps to reproduce
- Requested resets for test user A and for an unregistered email.
- Used A's reset link once, then again.
Evidence
- request and response: evidence/au-01-request.txt
- screenshot: evidence/au-01-screenshot.txt
AU-02 · Logging out revokes the session on the server, and any copied access token stops at its expiry
Pass
Steps to reproduce
- Copied user A's tokens, logged out with the app's button, and tried to refresh.
- Called /rest/v1/bookings with the old access token until it stopped working.
Evidence
- request and response: evidence/au-02-request.txt
- code excerpt: evidence/au-02-code.txt
AU-03 · Sessions and access tokens expire on a defined schedule
Pass
Steps to reproduce
- Decoded an access token and asked for the session settings.
Evidence
- settings screenshot: evidence/au-03-settings.txt
AU-04 · Repeated login and code attempts are rate limited
Pass
Steps to reproduce
- Asked for the auth rate-limit settings.
- Made 10 wrong-password attempts on test user A on staging.
Evidence
- settings screenshot: evidence/au-04-settings.txt
- request and response: evidence/au-04-request.txt
PM-02 · Prices and totals are set on the server
Pass
Steps to reproduce
- Edited the price and amount fields in the request that starts checkout.
- Read the function that creates the checkout session and the money columns.
Evidence
- request and response: evidence/pm-02-request.txt
- code excerpt: evidence/pm-02-code.txt
- screenshot: evidence/pm-02-screenshot.txt
BR-01 · Database backups are switched on and a restore has actually been tried
Pass
Steps to reproduce
- Asked for the backup settings and for a recent test restore.
Evidence
- settings screenshot: evidence/br-01-settings.txt
- screenshot: evidence/br-01-screenshot.txt
EL-01 · Error messages shown to users do not reveal internals
Pass
Steps to reproduce
- Sent malformed booking data, asked for a missing booking, called a function with no token.
Evidence
- screenshot: evidence/el-01-screenshot.txt
- request and response: evidence/el-01-request.txt
- code excerpt: evidence/el-01-code.txt
EL-02 · Passwords, tokens, card data and personal data are not written to logs
Fail
Steps to reproduce
- Searched the code for logging calls.
- Made a test payment on staging and asked for the matching function log lines.
- Expected
- Webhook logs hold the event id and type only
- Actual
- The full Stripe event, with the customer's email and phone number, is written to the function logs
Evidence
- file listing: evidence/el-02-files.txt
- screenshot: evidence/el-02-screenshot.txt
EL-03 · Failed logins and refused access attempts are recorded
Pass
Steps to reproduce
- Made 5 failed logins on test user A and one refused insert as user B, noting the times.
Evidence
- screenshot: evidence/el-03-screenshot.txt
- settings screenshot: evidence/el-03-settings.txt
TP-01 · Dependencies have no known critical or high vulnerabilities left unaddressed
Pass
Steps to reproduce
- Ran npm audit --omit=dev against package-lock.json.
Evidence
- screenshot: evidence/tp-01-screenshot.txt
- file listing: evidence/tp-01-files.txt
TP-02 · Personal data sent to analytics, trackers or chat widgets matches the privacy policy
Pass
Steps to reproduce
- Walked the main screens with the network tab open, listing third-party domains.
- Compared what reached each tracker with the privacy policy.
Evidence
- request and response: evidence/tp-02-request.txt
- screenshot: evidence/tp-02-screenshot.txt
DX-06 · Database functions called from the browser follow the same access rules as their tables
Pass
Steps to reproduce
- Listed the database functions in the public schema with their security setting.
- Called each signed out and as user B with user A's ids.
Evidence
- request and response: evidence/dx-06-request.txt
- settings screenshot: evidence/dx-06-settings.txt
PD-01 · Deleting an account removes the user's records, files and sessions
Pass
Steps to reproduce
- Created test user D with a booking and an uploaded photo, kept D's tokens, and deleted the account from Settings.
Evidence
- screenshot: evidence/pd-01-screenshot.txt
- request and response: evidence/pd-01-request.txt
- code excerpt: evidence/pd-01-code.txt
PD-03 · Private uploads are not publicly reachable
Pass
Steps to reproduce
- Listed storage buckets.
- Uploaded a private photo as user A and opened its address signed out and as user B.
Evidence
- settings screenshot: evidence/pd-03-settings.txt
- request and response: evidence/pd-03-request.txt
- screenshot: evidence/pd-03-screenshot.txt
SC-01 · The app publishes a working way to report security problems
Pass
Steps to reproduce
- Requested /.well-known/security.txt on the app's domain.
- Asked the owner to confirm a test message arrived.
Evidence
- request and response: evidence/sc-01-request.txt
- screenshot: evidence/sc-01-screenshot.txt
Outside this review
- Load testing
- The cleaners' mobile app
I ran every test in this report myself, on the build named above, from the test accounts named in the steps. Any AI help is listed above; I checked its output, and every result is my own.
— Sam Example, 2026-10-20
Checking a real report
A real report comes as a folder: report.json, the evidence folder, and a delivery folder holding this page, the exact checklist followed and fingerprint.txt, a list of every file's SHA-256. The report fingerprint above is the SHA-256 of that list, so any changed or swapped file gives a different fingerprint, and the registry page quotes it. Those files are not published for this sample.