Book a check-upBook
Menu

Records

Sample report

A shortened Launch Review of Ledgerly, a fictional invoicing app. The app is made up. The problems are typical of real AI-built apps.

Slop MedicPatient: Ledgerly (fictional)

Slop score
61
Critical
2
High
2
Medium
2
Sample readings for Ledgerly, a fictional app.

Patient chart

Ledgerly

Invoicing for freelancers. Fictional app, realistic findings.

Stack
Next.js App Router, TypeScript, Supabase, Stripe
Built with
Lovable, then Cursor
Size
8,900 lines
Review
Launch Review
Diagnosis
Triage red. Treat the three problems under "Treat today" first.

Triage

Slop Medic

Patient
Ledgerly (fictional)
Size
8,900 lines

Slop score61

Problems found and how urgent they are
ProblemUrgency
Open database tables 3Critical
Fakeable payment webhooks 1Critical
Unchecked user input 4High
Exposed secret keys 0None
any types 143Medium
Swallowed errors 12Medium
Copy-pasted code 14%Low
Tests 6%High

Slop score: 0 is clean, 100 is pure slop. Your report says where each problem is and how to fix it.

Red: fix these first.

How this review was made

Software
Automated checks for leaked keys, vulnerable and made-up packages, type errors and common security patterns, then an AI pass over the whole codebase that produced 38 candidate problems.
Human
Every candidate checked in the code. 9 were false alarms and removed. 3 problems the tools missed were added, including the Stripe webhook.
Proof
Both critical problems confirmed on the live app with two test accounts. No real user data was changed.
Human time
3 hours 40 minutes, including the proof and the call

Summary

Ledgerly works, and the invoicing logic itself is better than most AI-built apps I see. The problems are in what surrounds it.

Three issues need fixing before you onboard more customers: any signed-in user can read and edit every other customer’s invoices, the Stripe webhook can be faked to mark any invoice as paid, and the create-invoice action accepts whatever the browser sends. All three are small, contained fixes. Everything else can wait for a quiet week.

Treat today

Critical

Any signed-in user can read and edit every invoice

supabase/migrations/0004_invoices.sql:18

The select and update policies on invoices allow every row. Your publishable key (the anon key, in older projects) is public by design, so anyone with an account can list all invoices of all customers from the browser console, including names, addresses and amounts.

Proven on the live app: I signed up two test accounts. From the browser console, account B listed every invoice of account A, including the customer address. Screenshot in the full report.

create policy "Invoices are readable"
  on invoices for select to authenticated
Removed:   using (true);
Added:   using (auth.uid() = owner_id);

Treatment: Restrict select, update and delete to the owner, then repeat for invoice_items and customers. Takes about an hour including a test with two accounts.

Prescription for your AI toolSlop Medic

In supabase/migrations, add a new migration that replaces the select, update and delete policies on invoices, invoice_items and customers so each only allows rows where owner_id = auth.uid(). For invoice_items, check ownership through the parent invoice. Do not change any application code. List every policy you changed.

Signed

Critical

The Stripe webhook believes anyone

app/api/webhooks/stripe/route.ts:4

The webhook parses the request body without checking Stripe’s signature. Anyone who finds the URL can send a fake checkout.session.completed event and mark any invoice as paid.

Proven on the live app: A hand-written request to the webhook URL marked one of my test invoices as paid. No money changed hands. Request and screenshot in the full report.

export async function POST(req: Request) {
Removed:   const event = await req.json()
Added:   const signature = req.headers.get('stripe-signature') ?? ''
Added:   const event = stripe.webhooks.constructEvent(
Added:     await req.text(), signature, env.STRIPE_WEBHOOK_SECRET
Added:   )
  if (event.type === 'checkout.session.completed') {

Treatment: Verify the signature with the raw body and your webhook secret, and return 400 when it fails.

Prescription for your AI toolSlop Medic

In app/api/webhooks/stripe/route.ts, verify the Stripe signature before handling any event: read the raw body with req.text(), call stripe.webhooks.constructEvent with the stripe-signature header and STRIPE_WEBHOOK_SECRET, and return a 400 response if verification throws. Keep the existing event handling unchanged.

Signed

High

Creating an invoice trusts the whole request

app/invoices/actions.ts:12

The server action inserts whatever object the form sends. A user can set owner_id to someone else, or create an invoice that is already marked as paid.

'use server'
Removed: export async function createInvoice(data: any) {
Removed:   return supabase.from('invoices').insert(data)
Added: export async function createInvoice(input: unknown) {
Added:   const { customerId, lines } = InvoiceInput.parse(input)
Added:   const user = await requireUser()
Added:   return supabase.from('invoices').insert({
Added:     customer_id: customerId, lines, owner_id: user.id, status: 'draft'
Added:   })
}

Treatment: Validate the input with a schema and set owner and status on the server. The same pattern applies to updateInvoice and createCustomer.

Prescription for your AI toolSlop Medic

In app/invoices/actions.ts, change createInvoice to accept unknown, parse it with a zod schema that only allows customerId and lines, get the user from the session, and insert owner_id = user.id and status = 'draft' on the server. Apply the same pattern to updateInvoice and createCustomer. Add the schemas to lib/validation.ts.

Signed

Treat when you can

Medium

The dashboard runs one query per customer

app/dashboard/page.tsx:31

Totals are loaded one customer at a time, so an account with 200 customers makes 201 database calls on every page load. It feels fine today and gets slower with every customer. Replace it with one grouped query.

Medium

Strict mode is off and any is everywhere

tsconfig.json:7

strict is false and there are 143 uses of any, most at the boundary where data comes in. TypeScript is not checking the parts most likely to break. Turn strict on and fix the types at the edges first.

Medium

A deprecated auth package

package.json

@supabase/auth-helpers-nextjs is deprecated and no longer supported. Fixes and security updates go to its replacement, @supabase/ssr.

Low

Three date libraries

package.json

moment, date-fns and dayjs are all installed and all used. Keep date-fns, remove the other two and about 80 KB of compressed JavaScript from every page.

Healthy

  • Sign-in with Supabase Auth is set up correctly, including email confirmation and session refresh.
  • Stripe Checkout itself is integrated the standard way. Only the webhook needs work.
  • Tax and rounding in the invoice totals are correct and covered by the few tests that do exist.
  • The component structure is consistent enough that the fixes above will not ripple through the UI.

Discharge instructions for your AI

Paste these into your Lovable knowledge, Cursor rules or CLAUDE.md so the next session doesn’t bring the same problems back.

- Every server action and route handler validates its input with a schema from lib/validation.ts.
- Never use the secret key (the service role key, in older projects) outside supabase/functions.
- Every new table gets row level security and an owner policy in the same migration.
- No new dependencies without asking first.

The full report continues with 25 more problems, the call notes and a fix order. Once the three problems above are fixed, Ledgerly gets a free follow-up check. Book a review to get one for your app.

Get one for your app.

From $149 for the first 10 customers (10 left). Ready within 2 business days. If the Safety Scan finds no problem rated medium or worse, you pay nothing.

Visiting hoursBook any time. I reply within one business day.