Review report (sample)
- Subject
- A booking service for small studios and salons (fictional). It was built with an AI coding tool and uses Supabase for the database and sign-in, and Stripe to sell class passes by card. It has not launched yet.
- How it is judged
- Written the way a manual review is: an engineer reads the code and makes the call. None of it was generated by a tool.
Sample: a format example built around a fictional app (a booking service made with AI). It is not a real client or a real review result.
Deliverable 1A verdict on the critical holes: missing RLS, exposed secrets, absent ownership checks, double-charging payments
Verdict: every area has a hole worth closing
Each area gets a verdict on whether a hole exists. This review looks only for critical holes, the kind an app should not launch with, so every hole it finds is critical. The order to fix them in reflects what could actually happen in this app.
| Area | Verdict |
|---|---|
| Database access rules (RLS) | Hole foundSeverity CriticalPriority 2 |
| Exposed secrets | Hole foundSeverity CriticalPriority 1 |
| Ownership checks | Hole foundSeverity CriticalPriority 3 |
| Double-processed payments | Hole foundSeverity CriticalPriority 4 |
Because this is a sample, every area has a finding, so you can see how each one is written up. In a real review, an area with no hole is reported as “none found.”
Sample: a format example built around a fictional app (a booking service made with AI). It is not a real client or a real review result.
Deliverable 2A report written for non-engineers (a few pages, jargon translated)
Findings: what is happening, and what it means for the business
Jargon is translated where it appears. Findings are ordered by priority, the first to fix first.
Priority 1Severity CriticalManual reviewExposed secrets
The payment secret key is in code sent to the browser
- What was found
- The secret key for the payment service (Stripe) is written into code that is delivered to every visitor's browser. A secret key belongs on the server only; think of it as the key to the shop's safe. Anyone who opens the page can read it with the browser's developer tools.
- What it means for the business
- Whoever has the key can act as your account: issue refunds, read customer details, and so on. It is tied directly to money, and by the time you notice, it has already been used.
- How to fix it
- First, create a new key in the Stripe dashboard and make the old one unusable. Do this yourself rather than through the AI. A leaked key keeps working until it is replaced, which is why this comes first. Then change the code so the new key is used only by code that runs on the server.
- What was fine
- The public Supabase key in the browser code is fine: it is designed to be handed to browsers. Protecting the data is the job of the database rules in the next finding.
Priority 2Severity CriticalManual reviewDatabase access rules (RLS)
Anyone can read every booking without logging in
- What was found
- The table that stores bookings has no RLS (the database setting that decides who may see which rows). Using the public key that ships with the page, someone who is not logged in can read every customer's bookings: names, phone numbers and times.
- What it means for the business
- Your customers' personal details can be pulled out as a list from outside. If that happens, you are left contacting customers and repairing the service's reputation.
- How to fix it
- Turn on RLS for the bookings table and add a rule that each logged-in customer can read and change only their own bookings. If the shop needs a screen that shows every booking, grant that separately as an admin permission.
- What was fine
- The menu and opening-hours tables are readable by anyone, and that is fine: they are meant to be public. The question is not whether anyone can read something, but whether it would hurt if they did.
Priority 3Severity CriticalManual reviewOwnership checks
Changing a booking number lets someone cancel another customer's booking
- What was found
- The code that cancels a booking acts on whatever booking number the page sends. It never checks, on the server, that the booking belongs to the person who is logged in. The page only shows your own bookings, but send a different number and the cancellation reaches someone else's.
- What it means for the business
- Anyone who can log in can cancel other customers' bookings just by changing a number. Prank cancellations leave slots the shop cannot sell, and customers lose bookings without knowing why.
- How to fix it
- At the start of every cancel or change, check that the booking's owner is the person logged in, and refuse if not. Hiding something on the page does not protect it. This code also runs with a powerful key that bypasses the database rules, so fixing RLS in the previous finding does not close this hole.
- What was fine
- Creating a new booking was fine: the server attaches the logged-in customer's identity itself.
Priority 4Severity CriticalManual reviewDouble-processed payments
If the same payment notice arrives twice, the customer gets two passes
- What was found
- When a class pass is bought, Stripe sends a “payment completed” notice and the app adds sessions to the customer's balance. Depending on the network, the same notice can arrive more than once, and the app has no way to tell whether it has already handled one.
- What it means for the business
- If a notice arrives twice, one payment adds two passes' worth of sessions. You lose revenue without noticing, and correcting balances afterwards means awkward conversations with customers.
- How to fix it
- Record the number of each notice you receive (its event ID), and do nothing when a recorded number arrives again. Doing the same thing twice and still getting the result of doing it once is called idempotency.
- What was fine
- The amount charged was fine: the server sets it from the type of pass, so changing the price on the page does not change what is charged.
Sample: a format example built around a fictional app (a booking service made with AI). It is not a real client or a real review result.
Deliverable 3Prioritized fix instructions you can paste straight into Claude Code or Cursor
Fix instructions: paste them into your AI tool, highest priority first
Paste one, let it fix, check the app still works, then move to the next. That way you can tell which change did what.
Priority 1The payment secret key is in code sent to the browser
Example instruction to paste into your AI tool
Find every place where code delivered to the browser (client-side files or public environment variables) uses a secret key, and list them. Do not print any key values. Move that logic to the server side (for example, an API route) and read the key from a server-only environment variable.
Priority 2Anyone can read every booking without logging in
Example instruction to paste into your AI tool
Enable RLS on the bookings table in Supabase and add policies so that a logged-in user can only read and write their own bookings. Create the change as a new migration, and do not delete or modify any existing data. Before applying it, explain which rules you are adding to which tables.
Priority 3Changing a booking number lets someone cancel another customer's booking
Example instruction to paste into your AI tool
List every server-side function that reads, changes or cancels a booking, and check one by one whether it verifies on the server that the booking belongs to the logged-in user. Where that check is missing, add an ownership check and refuse requests from anyone else. Report any place that only hides data on the page.
Priority 4If the same payment notice arrives twice, the customer gets two passes
Example instruction to paste into your AI tool
Make the Stripe webhook handler safe against the same event arriving more than once. Store each processed event ID as a unique value, and when an event ID that is already stored arrives, do nothing and return a success response. Do not change any existing purchases or session balances.
Never add a key or password to an instruction. The AI only needs to know where to look and what to change.
Sample: a format example built around a fictional app (a booking service made with AI). It is not a real client or a real review result.
Deliverable 4A 30-minute call to walk through what is dangerous and what is fine
The call: how it would go for this sample
I walk you through what is dangerous and what is fine, out loud. For this sample, in this order:
- Starting with the highest priority: what happens if each one is left alone
- In what order to fix the rest before launch
- What was fine: the things you do not need to worry about