App Locksmith
Sample

This is a sample. Tallyroom isn't a real company and none of this describes a real client's app. I wrote it to show exactly what a report looks like, down to the wording.

Independent security review: Tallyroom

A scheduling and invoicing app for small studios, built with Claude Code, Next.js and Supabase. Reviewed over four days in February 2026, then fixed.

Prepared for
Tallyroom, attention of the founder
Prepared by
Nate Parker, App Locksmith
Work done
10 to 13 February 2026
Report dated
16 February 2026
Version
Final, version 1.0
Reference
AL-2026-009

The short version

I found eight problems. Seven are fixed and live. One is a decision for you, and it's the last section of this report.

Two were serious, and both come from the same place: the agent added a feature later and nobody wired it into the checks that were already there. Your admin pages would let in anyone who typed the address, and your app believed any message that claimed a customer had paid. Either one was enough to take your customer list or your revenue.

That pattern is worth more than the individual bugs. Your app has good security in the parts that were built first, and none in the parts that were added after. That's what building with an agent does, and it's why looking once a year isn't much use.

8problems found
7fixed by me
1needs your decision

How I rate them

  • CriticalSomeone could do this today, without skill or luck, and reach everybody's data.
  • HighReal damage, but it takes an account, effort or a particular sequence.
  • MediumWorth fixing soon. Limited damage, or it needs something else to go wrong first.
  • LowTidy-up. Little risk on its own, but it helps someone who is already looking.

What I looked at

  • Your code, as of commit 4d91b07 on the main branch.
  • The live site, used as a signed-in customer and as a stranger who never logged in.
  • Your Supabase project: database rules, authentication settings and stored functions.
  • Your server routes on Vercel, and the settings they read.
  • How Stripe is wired in, including what your app does when Stripe tells it something.

What I did not look at

  • Vercel, Supabase, Stripe and OpenAI themselves. I checked how you use them, not how they're built.
  • Your laptop, your email and your accounts. Most small companies are broken into through a person. That's a different job and worth doing.
  • Your customers' own systems, or anything they connect to Tallyroom.
  • Anything to do with your books, your contracts or your tax.

What I found

Each one says what it means for you, not what it's called in security. The fixes are already live unless the status says otherwise.

1

Anyone who typed the address got into your admin pages

CriticalFixed

What it means
Your customer dashboard checked that people were logged in. Your admin section, added three months later, didn't. Nothing in the app linked to it, which is why nobody noticed, but nothing hid it either.
What could have happened
Anyone who guessed the address, and it's the address everyone guesses, could read your full customer list, change invoices, issue refunds to themselves and delete accounts. No login, no trace beyond a web request that looked ordinary.
What I did about it
Moved the login check into one place that every page passes through, so a new page is protected by default and has to opt out on purpose. Added a test that visits every route signed out and fails the build if any of them answers with a page instead of a redirect.
How to check it yourself
Open your site in a private window and go to /admin. You should land on the login page.
Whether anyone used it
Your Vercel logs go back 30 days. In that window every request to /admin carried a session belonging to you or your co-founder. Before that I can't see, so I can't rule it out.

What I sawGET /admin/customers returned the full page with no session cookie present

2

Anyone could tell your app they had paid

CriticalFixed

What it means
When Stripe tells your app that a payment succeeded, it signs the message so you can tell it really came from Stripe. Your app never checked the signature. It believed anything that arrived at that address.
What could have happened
Anyone could have sent your app a made-up payment message and been upgraded to your top plan for nothing, repeatedly, on as many accounts as they liked. You'd have seen it as customers on paid plans who never appear in Stripe.
What I did about it
Your app now verifies the signature on every message and rejects anything unsigned or altered. It also treats Stripe as the source of truth about who has paid, rather than its own record, so the two can't drift apart again.
How to check it yourself
Compare your paid accounts against Stripe's subscriber list this month. They should match exactly.
Whether anyone used it
I reconciled every paid account against Stripe. All 61 have a real subscription. Nobody appears to have found this.

What I sawPOST /api/stripe/webhook accepted any JSON body, with no signature check against the signing secret

3

Your AI provider's key was sent to every visitor

HighFixed

What it means
A setting whose name starts with NEXT_PUBLIC_ is deliberately handed to every browser that loads your site. Your OpenAI key was stored under that name, so it was in the page source of your dashboard, readable by anyone who pressed view source.
What could have happened
Anyone who looked could have used your AI account for their own work, billed to you, at whatever rate they liked. This is the single most common way AI-built apps get an unexplained bill.
What I did about it
Moved the key to a server route the browser can't read, rotated it so the exposed one is dead, and set a monthly spending cap on the provider account.
How to check it yourself
Load your dashboard, view source, and search for sk-. There are no matches now.
Whether anyone used it
Your provider's usage log shows no spending outside your own working hours or patterns, going back to the key's creation.

What I sawNEXT_PUBLIC_OPENAI_API_KEY visible in the page source of /dashboard

4

Changing a number in the address showed someone else's invoice

HighFixed

What it means
Your invoice pages asked the database for invoice number 1042 and showed whatever came back. They never asked whether 1042 belonged to the person looking.
What could have happened
Any customer could have counted upwards through every invoice in Tallyroom: who your customers are, what they charge their clients, and how much they make. For a lot of studios that's the most commercially sensitive thing you hold.
What I did about it
Every invoice, booking and client record now loads with an ownership check in the database itself, so the rule holds even if a future page forgets to ask.
How to check it yourself
Signed in as one test account, change the number in an invoice address. You should get a not-found page. I left two test accounts in your project.
Whether anyone used it
Your logs don't record which rows were returned, so I can't say for certain. I saw no pattern of sequential requests that would suggest someone walked through them.

What I sawGET /api/invoices/1042 returned an invoice belonging to another account

5

Uploaded files were public, and the links were guessable

MediumFixed

What it means
Files your customers upload went into storage that anyone could read, under names like invoice-0042.pdf. No login needed, and no guessing needed either.
What could have happened
Anyone could have downloaded your customers' uploaded documents by counting. Nothing would have appeared in your app's logs, because the requests never touched your app.
What I did about it
Made the bucket private, gave every file a random name, and switched downloads to short-lived links that your app hands out only to the file's owner.
How to check it yourself
Take an old file link from your browser history and open it in a private window. It should be refused.

What I sawPublic bucket uploads, with sequential file names, readable without a session

6

Nothing stopped someone trying passwords all day

MediumFixed

What it means
There was no limit on how many times an address could try to log in, and the error message was different for an unknown email than for a wrong password, which tells an attacker which of your customers exist.
What could have happened
Someone could have worked through a list of leaked passwords against your customers at speed. People reuse passwords, so this works more often than it should.
What I did about it
Added a limit per address and per account, made both failures give the same message, and turned on alerts so you hear about a burst of failures.
How to check it yourself
Get your own password wrong six times in a row. You should be asked to wait.

What I sawNo rate limit on the sign-in and reset routes; distinct responses for unknown address and wrong password

7

A leftover debug page listed your settings

LowFixed

What it means
A route at /api/debug, added while building and never removed, listed the names of every setting your app reads. Not the values, but the names tell someone exactly what to go looking for.
What could have happened
On its own, nothing. It shortens the work for someone attacking the things above, which is the only reason it's here.
What I did about it
Removed the route and added a check that fails the build if a route named debug or test ever ships again.
How to check it yourself
Visit /api/debug. You should get your ordinary not-found page.

What I sawGET /api/debug returned the names of 23 environment variables

What needs your decision

This one isn't a bug. It's a choice about what you tell people, and it's yours to make, not mine.

8

Your app sends invoice text to an AI provider, and your privacy notice doesn't say so

MediumOpen, waiting on you

What it means
The “tidy up this description” button sends the text of an invoice line to OpenAI. That text routinely contains your customer's client names and what was done for them. Your privacy notice lists no AI provider and no transfer outside the UK.
What could have happened
Nothing technical. But a customer who reads the notice, then reads the network traffic, has been told something untrue, and studios handling client data may have promised their own clients otherwise. It's the kind of thing that surfaces in the first security questionnaire you're sent.
What I did about it
Nothing yet. I've written the three options and the wording for each, and left them in your handover notes. Changing what you tell your customers is your call.

What I sawPOST to api.openai.com containing invoice line text, from the tidy-up feature

Your options

  • Name it in your privacy notice: which provider, what is sent, where it goes. Half an hour of work and the honest default.
  • Strip client names before sending, so only your own wording leaves. Keeps the feature, removes most of the problem.
  • Turn the feature off. It's used by about one in nine of your customers, so this is cheaper than it sounds.

What I checked and found nothing wrong with

A report that only lists problems tells you nothing about what's solid. These were fine.

  • Card payments. Stripe handles them and card numbers never reach your systems.
  • Passwords. Stored by Supabase Auth, hashed properly, and your own code never sees them.
  • Your sign-up and password reset emails. Correct links, sensible expiry, no data leaking in the text.
  • The packages you depend on. None of the versions you ship have known holes as of the report date.
  • Your database backups. Running nightly, and the one I restored came back complete.

Keeping it this way

  • Assume every new page is unprotected until proved otherwiseBoth serious findings were features added after the security was built. That will keep happening. The tests I added are what catch it.
  • Keep the signed-out route test in your buildIf it fails, something new is reachable without logging in. Don't skip past it.
  • Reconcile paid accounts against Stripe monthlyTwo minutes, and it's how you'd notice the payment problem coming back.
  • Check the spending cap each quarterIt's set for today's usage. Raise it on purpose, not by removing it.

What this report is, and what it isn't

  • It's a record of what I checked, what I found and what I changed, on the dates given. It isn't a certificate, and it isn't a guarantee.
  • Nobody honest can tell you an app has no security holes. I can tell you what I looked for, what I found, and what I fixed, and you can hand that to anyone who asks.
  • Every problem here is described after its fix went live, except the open one, which is described as it stands.
  • I reviewed your code and your live app. I did not review the security of Vercel, Supabase, Stripe or OpenAI themselves.
  • If you share this report, share all of it. Individual findings pulled out of it will read as worse or better than they are.

Nate Parker

App Locksmith, applocksmith.com

Signed 16 February 2026. Ask me anything about this report until 18 March 2026.

Want one of these for your app?

This is what you get: the plain-English version, the technical detail, the fixes already made, and a dated report you can hand to a customer or a buyer.