MATJAŽ KOROŠEC← BACK

Sample AI app audit: full report

An audit of an open-source ordering app built with Lovable, React and Supabase: 21 findings, the 5 worst risks in plain words, and what to fix first.

1. Cover

AppA website and ordering app for a small cafe: menu, table reservations, dine-in and takeaway orders with optional card payment (Stripe), and a staff dashboard with a menu editor and CSV import. Built with Lovable.
DateOctober 26, 2026
StackReact 19, TypeScript (strict mode), TanStack Start 1.168.32, Tailwind CSS 4, Supabase (database, login, row level security), Stripe Checkout through the Stripe REST API, hosted on Cloudflare through Lovable.
SizeAbout 4,300 lines of the app's own code (not counting generated UI kit files), 9 database migrations, 5 database tables, 482 locked packages, 50 commits written by the Lovable bot over 2 days (August 12 and 13, 2026).
Access usedThe public source code repository only. Static review: I read the code and the database migrations. No live database access, no access to the Supabase or Stripe dashboards, no login to the app, no requests sent to the app or its database. No code from the repository was installed, built or run.
Tools usedgit (full history of all 50 commits, every diff read), grep and ripgrep for secrets and patterns, a short script that sent all 482 locked package versions in bun.lock to the public OSV.dev vulnerability database, and line by line reading of every own source file and migration.
Not used, and whynpm audit (the project has no package-lock.json, it uses Bun), ESLint and the TypeScript compiler (both need an install, which I do not do on code I do not own), Lighthouse (needs the app running), gitleaks (replaced by a manual secret search of the files and the whole git history), the Supabase security advisor (needs project access).
Audited byMatjaž Korošec (koroCode)

This is a sample. The app is a public open-source project, and its name and author are removed, together with the admin email and password it exposes. File names, line numbers and code names are left out too, so the app cannot be traced. The paid report gives the exact file and line for every finding. A paid audit also covers the live app, the database settings and the deploy setup, with the owner's access.

2. Summary for the founder

Verdict: fix it. Nothing needs a rebuild. The bones are good: every table has row level security turned on, prices are worked out on the server and not trusted from the browser, the Stripe webhook checks its signature, and TypeScript runs in strict mode. The problems are a handful of shortcuts, and most take under half a day to close.

The 5 worst risks, in plain words

  1. Anyone can log in as the admin. The staff sign-in page fills in the admin email and password for you and prints them under the form. That account can see every guest's name, phone number and email, and change prices or hide and delete menu items.
  2. Anyone can create an order marked "paid". The database lets any visitor write straight into the orders table, past the app, and choose every field, including "paid" and the total. The kitchen screen then shows it as "Paid online". That is free food.
  3. The web framework has a known critical security hole. The version of TanStack Start in use has a published cross-site scripting advisory. Staff login sessions live in the browser's storage, which is exactly what such a hole can steal.
  4. Bots can flood the kitchen. Orders, table requests and sign-ups have no rate limit and no bot check. A simple script can fill the order screen with fake dine-in orders during a lunch rush.
  5. An order can show "paid" before the money arrives. The payment handler marks an order paid when checkout finishes, without checking that the payment actually went through. For slower payment methods those are 2 different moments.

Fix this week, before anything else

  1. Remove the demo login from the sign-in page and change the admin password.
  2. Stop visitors from writing to the orders, order items and reservations tables directly. Only the server should write there.
  3. Update TanStack Start and its serializer to the fixed versions.

All 3 together are about 2 days of work.

3. How I checked

Every finding below comes from the code itself, with the file and line where it lives. Nothing was guessed from the outside, and nothing was tried against the live app.

AreaWhat I checkedHow
SecretsAPI keys, Stripe keys, webhook secrets, database service keys, tokens, cloud keysSearched every file and every one of the 50 commits for known key shapes (service_role, sb_secret_, sk_live, sk_test_, whsec_, JSON web tokens, AWS, GitHub and Google keys).
Database rulesWho can read, write, change and delete each of the 5 tablesRead all 9 migrations in order and worked out the final rules, since later migrations drop and replace earlier ones.
Login and rolesSign-in, sign-up, password reset, who becomes staffRead the sign-in page, the auth middleware, the staff check and the migration that hands out roles.
MoneyPrices, totals, order saving, Stripe Checkout, the webhook, tax, currencyFollowed one order from the cart to the kitchen screen, then one payment from Stripe back to the order.
InputWhat the server accepts from the order and table formsRead every validation schema on the server and compared it with the database checks and the forms.
AbuseRate limits, bot checks, sign-up controlsSearched the code for any limit, captcha or bot check on public writes.
DependenciesKnown security advisories in all 482 locked packagesSent the exact versions from bun.lock to OSV.dev and read each advisory that came back.
DeliverySettings file, build, tests, CI, rollbackCompared the example settings file with what the code reads, looked for tests, CI and a deploy plan.
Code healthCopies, dead code, loose types, running costsRead every own file, counted copies and unused files, checked polling and data loading.

4. Findings at a glance

21 findings: 2 Critical, 4 High, 9 Medium, 6 Low.

Severity: Critical is a data leak or money at risk; fix before the next user signs up. High breaks things for users or blocks growth. Medium slows every future change. Low: clean up when you are in that code anyway. Effort: S is under half a day, M is half a day to 2 days, L is more than 2 days.

#WhereProblemWhat can happenSeverityFixEffort
1Staff sign-in page, database seedThe sign-in form comes pre-filled with the admin email and password, and the page prints them as "Demo access (temporary)". A migration gives that account the admin and staff roles.Anyone reads every reservation and order with names, phone numbers, emails and notes, changes prices, and hides or deletes menu items.CriticalRemove the pre-filled values and the printed line, change the password today, check the login log for strangers.S
2Database rules, orders and order items tablesVisitors may insert any row with any values ("with check (true)"), and the public key ships in the browser, as it always does with Supabase. The app's own order form goes through the server, but nothing forces it to.A script posts an order with payment status "paid", any total and any item prices. It appears on the staff screen as "Paid online".CriticalLet the server write orders with its private key (or one database function), remove insert rights for visitors, and refuse "paid" from anything but the payment webhook.M
3DependenciesTanStack Start is pinned to a version with a published critical advisory (reflected cross-site scripting in server function responses, fixed in a later patch). The serializer it uses (seroval) has 1 critical and 1 high advisory. Zod and a few build tools have more.A crafted link runs script in a staff member's browser and takes over their logged-in session, which sits in browser storage.HighUpdate TanStack Start, seroval and zod, then click through the order, payment and staff flows.S
4Database rules, reservations tableSame open insert rule as finding 2. A direct insert skips the server's checks and can set the status to "confirmed".Fake confirmed bookings fill the reservation list, and staff hold tables for no one.HighSame change as finding 2, applied to reservations.S
5Order form, table request form, staff sign-upNo rate limit and no bot check anywhere. Dine-in orders go straight to the kitchen screen without payment.A bot fills the kitchen screen with hundreds of fake orders, or the reservation list with fake guests.HighAdd a bot check (for example Cloudflare Turnstile) and a per-visitor limit on the 3 server functions.M
6Payment webhookThe order is set to "paid" on "checkout completed" without checking the session's payment status. With delayed payment methods, checkout completes before the money moves.Staff hand over an order that is later not paid.HighMark paid only when the session's payment status is "paid", or on the "async payment succeeded" event.S
7Payment setupStripe automatic tax is switched on in code. The cart shows a total without tax, and the saved order total has no tax either.Customers see a higher price at checkout than in the cart. If Stripe Tax is not set up on the account, Stripe refuses every checkout and takeaway ordering stops.MediumDecide on tax with the owner, then either turn it off in code or show and save the tax.S
8Payment webhookEvery "paid" event also resets the order status to "new". Stripe may send the same event more than once.An order already marked "ready" jumps back to "new" and the kitchen makes it twice.MediumOnly set the status when the order is not paid yet. Store the event id and ignore repeats.S
9Placing an orderThe order and its items are saved in 2 separate writes, with no transaction. A failed write of the payment reference is ignored.An order with no items shows up on the kitchen screen, and a refund is hard to trace.MediumSave both in one database function, check every write.M
10Order codesPickup codes are 6 characters from 16 possible each, with no unique rule in the database.After about 4,800 orders there is a 50% chance 2 orders share a code. The order status page then fails and says "Order not found".MediumAdd a unique rule and retry on a clash, or use a longer code.S
11Table request checks on the serverThe server only checks that the date looks like a date. Past dates pass. Time slots run to 17:00 every day, but the cafe closes at 16:00 on Sundays and at 17:00 Monday to Wednesday. There is no limit per time slot.Guests book tables for closed hours or yesterday, and the cafe gets overbooked at busy slots.MediumCheck date, opening hours and capacity on the server, from one shared list of hours.M
12LoginThere is no password reset anywhere, and anyone can create an account from the staff page. New accounts get no rights, which is correct, but nothing stops sign-up spam.A staff member who forgets the password is locked out until someone edits the database.MediumAdd "Forgot password", and turn off open sign-up (invite staff instead).M
13Deploy settingsThe example settings file names a key the code never reads and leaves out the server key that the order status page and the payment webhook need. There is no CI, no staging and no documented way back. Changes go live when Lovable syncs the branch.A new developer or a second deployment follows the setup guide and gets a broken order status page and webhook. A bad change reaches customers with no check.MediumFix the example file, add a build and test step before deploy, write down how to roll back.S
14TestsNo tests at all, and no test script.Every change to ordering or payments is checked by hand, or by customers.MediumAdd tests for the 3 money paths first: placing an order, the webhook, the signature check.M
15CodeThe same Supabase connection helper is copied into 6 files, and a shared version that was meant to replace the copies is never used. The 4 menu categories are typed out in 8 places, including the database.Every change has to be made in many places, and one missed copy is a bug. Adding a category means 8 edits and a migration.MediumOne shared helper, one categories list.M
16Menu loadingWhen the menu fails to load, the server returns an empty list and the page says "No items in this category yet."Visitors think the cafe has no menu, and nobody knows the database is down.LowShow an error message with a retry button, and log it.S
17RepositoryThe .env file is committed although the ignore file lists it, and the README says it is not committed. Today it holds only public keys.The next secret someone adds there goes public in git history.LowRemove it from git, keep only the example file.S
18Code33 of the 46 generated UI components are never used, and 7 packages exist only for them.No speed cost, since unused code is dropped from the build, but more to read and more to update.LowDelete them.S
19Running costsEvery open homepage reloads the menu every 30 seconds and on every tab switch.About 120 server calls an hour for each idle tab. Small now. With traffic, it shows up on the bill.LowReload every few minutes, or only when the menu changes.S
20Types and checks2 places use the any type for the database client. One staff action skips the app's own staff check and relies on the database alone, so a non-staff user gets a "done" message for a change that did not happen.Hidden type errors, and a confusing message.LowType the client, add the same staff check as the other actions.S
21Money formatDollars and USD are hard coded in 2 files.A cafe outside the US needs code changes to sell in its own currency.LowOne currency setting.S

5. Every finding in detail

Each finding has where it lives, what the code does, why it matters, how to fix it and how to check that the fix works.

1. Anyone can log in as the admin

SeverityCritical
EffortS, under 2 hours plus the login log review
WhereThe staff sign-in page, and the database migration that hands out roles

What I found. The sign-in form keeps its email and password in React state, and both start with the admin's real values. Under the form, the page prints the same email and password as "Demo access (temporary)". One of the database migrations looks up that email among the login accounts and gives it both the admin and the staff role.

Why it matters. A stranger opens the staff page, presses Sign in and lands on the staff dashboard. There they see every reservation with name, phone, email and notes, and every order with name, phone and table. The same account can edit prices, mark items sold out, hide them and delete them. If the cafe is in the EU, names, phone numbers and emails are personal data under the GDPR, and a leak like this may have to be reported.

How to fix it.

  1. Start both fields empty.
  2. Delete the "Demo access" line.
  3. Change the admin password in the Supabase dashboard today, before the code change goes live.
  4. Read the Supabase auth log for sign-ins to that account from unknown places since the app went live, and tell the owner what was found.

How to check the fix. Open the sign-in page in a private window: both fields are empty and no credentials are printed. Signing in with the old password fails.

2. Anyone can create an order marked "paid"

SeverityCritical
EffortM, about 1 day
WhereThe database rules for the orders and order items tables, and the server function that places an order

What I found. A migration grants insert rights on the orders and order items tables to anon, the role every visitor has, and the insert rules use WITH CHECK (true), so every column can hold any value. The public Supabase key is in the browser bundle, as it is in every Supabase app, so any visitor can talk to these tables directly. The app's own order function also writes with the public key, which is why the open rule exists: the server itself needs it.

Why it matters. The order function works out prices on the server, but a direct write skips it. Someone can save an order with the payment status set to paid, any total, and items at any price. The staff screen lists every order that is not waiting for payment and labels this one "Paid online". The kitchen makes it, the counter hands it over.

How to fix it.

  1. Change the order function to write with the server-only database client, which already exists in the code. A single database function that saves the order and its items is an equally good choice and also fixes finding 9.
  2. In a new migration, drop both insert rules and revoke insert rights on both tables from anon and authenticated.
  3. Keep the payment status writable only by the server: new orders start as waiting for payment or pay at the counter, and only the webhook sets paid.

How to check the fix. With the public key, a direct insert into the orders table returns a permission error. Placing a dine-in and a takeaway order through the site still works, and a test payment still turns the takeaway order to paid.

3. The web framework has a known critical security hole

SeverityHigh
EffortS, half a day including a full click-through
Wherepackage.json and bun.lock

What I found. @tanstack/react-start is pinned to exactly 1.168.32, with @tanstack/start-server-core 1.169.17 under it. OSV.dev lists GHSA-qx66-fv34-fjm8 for both: reflected cross-site scripting in server function responses, rated critical, fixed in 1.168.60 and 1.169.39. The serializer seroval 1.5.6 has GHSA-p6vx-979v-rg4c (critical) and GHSA-jp82-f5mq-hwhp (high). zod 3.25.76 has GHSA-mrhx-c7xc-m3w5 (high, memory use). The full list is in section 6.

Why it matters. The Supabase client stores the staff login session in localStorage. Any script that runs on the page can read it. A staff member who opens a crafted link hands over their session, and with it everything in finding 1.

How to fix it.

  1. Update @tanstack/react-start to at least 1.168.60 and let @tanstack/start-server-core, seroval and zod move to their fixed versions.
  2. Run the build and click through ordering, payment and the staff dashboard, since TanStack Start changes quickly between patches.

How to check the fix. Run the same OSV.dev check on the new bun.lock: no critical or high advisories left in packages that reach the browser or the server.

4. Fake confirmed bookings

SeverityHigh
EffortS, done together with finding 2
WhereThe database rules for the reservations table, and the server function that saves a table request

What I found. Same pattern as finding 2: insert rights for anon and an insert rule with WITH CHECK (true). The server's form check only runs when the app's own form is used. The database allows the statuses pending, confirmed and cancelled, so a direct write can choose confirmed.

Why it matters. Staff see bookings that look already confirmed by someone on the team, and hold tables for people who do not exist.

How to fix it. Write reservations from the server with the server-only client, then revoke insert rights from anon and authenticated. If the open rule has to stay for some reason, at least limit it to new requests with the status pending.

How to check the fix. A direct insert with the public key fails. A table request through the site still arrives as pending.

5. Bots can flood the kitchen

SeverityHigh
EffortM, about 1 day
WhereThe server functions for orders and table requests, and the staff sign-up form

What I found. A search of the whole code finds no rate limit, no captcha and no bot check. Dine-in orders are saved as pay at the counter and appear on the staff screen straight away, with no payment step in between.

Why it matters. The order form takes up to 30 items per order and has no limit on how often it is sent. A short script can put hundreds of fake dine-in orders on the kitchen screen in a minute, right at lunch, and fill the reservation list the same way.

How to fix it.

  1. Add Cloudflare Turnstile to the order form, the table form and sign-up, and check its token on the server in each of the 3.
  2. Add a simple per-visitor limit on the 2 public server functions, for example 5 orders and 3 table requests per 10 minutes from one address.
  3. Turn on the rate limits and captcha in the Supabase auth settings.

How to check the fix. Sending the order form 10 times in a row from one browser gets refused after the limit, and a request without a valid Turnstile token is rejected by the server.

6. An order can show "paid" before the money arrives

SeverityHigh
EffortS, about 2 hours
WhereThe Stripe webhook

What I found. The webhook treats checkout.session.completed and checkout.session.async_payment_succeeded the same way and marks the order paid for both. It never reads the session's own payment_status.

Why it matters. For card payments the 2 moments are the same. For delayed methods (bank debits and similar, if they are ever switched on in Stripe), Stripe sends checkout.session.completed with payment_status set to unpaid, and only later sends async_payment_succeeded or async_payment_failed. The kitchen screen says "Paid online" for an order that may never be paid.

How to fix it. On checkout.session.completed, mark paid only when the session's payment_status is paid. Keep async_payment_succeeded as the second way to become paid, and async_payment_failed as failed.

How to check the fix. With the Stripe CLI, send a test checkout.session.completed event with payment_status unpaid: the order stays waiting for payment. Send async_payment_succeeded: it becomes paid.

7. The checkout price can differ from the cart

SeverityMedium
EffortS, after a short decision with the owner
WhereThe Stripe Checkout setup, the cart and the order function

What I found. Checkout is created with automatic_tax[enabled]=true. The cart total and the saved order total are both the plain sum of the items, without tax.

Why it matters. Where tax is added on top, the customer sees one price in the cart and a higher one at Stripe, which looks like a trick and loses orders. If Stripe Tax is not set up on the Stripe account, Stripe refuses to create the checkout. The customer then sees a "could not start the payment" error on every takeaway order, and an unpaid order is left behind in the database each time.

How to fix it. Ask the owner whether menu prices already include tax. If they do, which is normal for cafes in Europe, remove the automatic tax setting. If they do not, show the tax in the cart before checkout and save the final amount from Stripe on the order.

How to check the fix. Place a takeaway order in Stripe test mode: the amount on the Stripe page matches the cart to the cent.

8. A paid order can jump back to "new"

SeverityMedium
EffortS, about 2 hours
WhereThe Stripe webhook

What I found. When a payment arrives, the webhook also sets the kitchen status back to "new", and it does this by order id, whatever state the order is in. No event id is stored, so the same event is handled again each time it arrives.

Why it matters. Stripe can send an event more than once, and retries it when the webhook is slow to answer. If the kitchen has already moved the order to "preparing" or "ready", a repeat puts it back to "new", and it can be made twice.

How to fix it. Only update orders that are not paid yet, and leave the kitchen status alone when the order already moved on. Store each processed Stripe event id in a small table and skip ids already seen.

How to check the fix. Send the same test event twice with the Stripe CLI after moving the order to "ready": it stays "ready".

9. An order can be saved without its items

SeverityMedium
EffortM, about 1 day, less if done together with finding 2
WhereThe order function

What I found. The order is saved first and the items in a second, separate write. If the second write fails, the function returns an error to the customer but leaves the order in the database. The Stripe session reference is saved afterwards without looking at the result.

Why it matters. A dine-in order with no items appears on the kitchen screen. The customer, told it failed, sends it again. And when the payment reference is not saved, the payment is hard to match to its order when a refund or a dispute comes in.

How to fix it. Save the order and its items in one database function, which runs as 1 transaction: both are saved or neither is. Check the result of the payment reference write and log any failure.

How to check the fix. Force the item write to fail in a test (for example a quantity of 0): no order is left behind.

10. Two orders can share a pickup code

SeverityMedium
EffortS, about 2 hours
WhereThe order function, the orders table, and the order status page

What I found. The pickup code is the first 6 characters of a random id, each from 16 possible values: 16,777,216 codes in total. The code column has no unique rule. The order status page looks the code up with .maybeSingle(), which returns an error when 2 rows match, and the page then says the order was not found.

Why it matters. Codes clash much sooner than the total suggests (the birthday problem): after about 4,800 orders there is a 50% chance that 2 orders share a code. That customer's status page says "Order not found" while they wait at the counter.

How to fix it. Add a unique index on the code and try a new code when the save hits it. Or use 8 characters, which pushes the 50% point to about 77,000 orders, and still add the unique index.

How to check the fix. Saving 2 orders with the same code in a test is refused by the database, and the server retries with a new code.

11. Tables can be booked for closed hours and past dates

SeverityMedium
EffortM, about 1 day
WhereThe server check for table requests, the opening hours on the homepage, and the table form

What I found. The server checks the date only against the pattern \d{4}-\d{2}-\d{2}. The "not in the past" rule exists only in the browser form, so it is skipped by any direct request. The time slots are 08:00 to 17:00 for every day of the week, while the opening hours on the homepage end at 4:00 PM on Sunday and 5:00 PM Monday to Wednesday. There is no limit on bookings per slot. A date that matches the pattern but does not exist, like 2026-99-99, reaches the database and comes back as a general error.

Why it matters. Guests can ask for a table at 17:00 on a Sunday, an hour after closing, or for yesterday. Staff have to sort these out by phone, and busy slots can be booked far past the room's size.

How to fix it. Keep one list of opening hours and use it for both the homepage and the server. On the server, check that the date is real, not in the past, within opening hours for that weekday, and under the number of guests the room holds for that slot.

How to check the fix. A request for yesterday, for a closed hour, or for a full slot gets a clear message, and nothing is saved.

12. No password reset, and anyone can sign up

SeverityMedium
EffortM, about 1 day
WhereThe staff sign-in page and the staff dashboard

What I found. The Supabase password reset (resetPasswordForEmail) is not called anywhere in the code, and there is no page to set a new password. The staff sign-in page has a "Create account" tab open to anyone. New accounts get no role, which is correct, and see a "no staff access yet" message.

Why it matters. A staff member who forgets the password is locked out until someone changes it by hand in Supabase. The open sign-up tab adds accounts nobody needs and, with no bot check (finding 5), invites spam.

How to fix it. Add a "Forgot password" link and a page to set the new one. Turn off public sign-up in Supabase and add staff by invite from the dashboard.

How to check the fix. The reset email arrives and the new password works. The "Create account" tab is gone, and a direct sign-up call is refused.

13. The setup guide gives a broken app

SeverityMedium
EffortS, half a day
WhereThe example settings file, the README and the deploy setup

What I found. The example settings file names the public key differently from the name the code reads. The server key (SUPABASE_SERVICE_ROLE_KEY) is missing from the example, but the code needs it for the order status page and the webhook. The README tells a new developer to copy the example file. There is no CI, no staging, and the repository's notes for AI agents say every commit to the connected branch syncs back to Lovable.

Why it matters. A developer who follows the README gets an app where the order status page and the payment webhook fail. Any change, from a person or from the AI builder, reaches customers with no build or test step in between, and there is no written way back.

How to fix it. Correct the names in the example file and add the server key with an empty value. Add a CI step that runs the type check, the existing lint script and the tests from finding 14 on every push. Write down how to roll back to the last good version.

How to check the fix. A fresh copy set up only from the README places an order and shows its status page.

14. No tests

SeverityMedium
EffortM, about 1.5 days for the 3 money paths
WhereThe whole repository

What I found. The project has scripts to run, build, preview, lint and format. There is no test script and no test file.

Why it matters. Every change to ordering or payments is checked by clicking through it, or not at all. Findings 6, 8 and 9 are exactly the kind of mistake a test catches before customers do.

How to fix it. Add Vitest and write tests for the order function (prices come from the database, a sold-out item is refused), the webhook (paid, unpaid, repeated event) and the signature check (good, wrong and expired signature).

How to check the fix. The tests run in CI and fail when one of these paths breaks.

15. The same code in many places

SeverityMedium
EffortM, about 1 day
Where6 files with the same connection helper, 8 places with the menu categories

What I found. The small fetch wrapper that fixes the Supabase key header is copied into 6 files. A shared helper was written to replace the copies and is never imported. The 4 menu categories are typed out in 8 places, one of them a database check.

Why it matters. A fix to the wrapper has to be made 6 times, and one missed copy is a bug that shows up in only one part of the app. A new category ("Drinks") means 8 edits and a migration.

How to fix it. Use the one shared helper everywhere and delete the copies. Keep the categories in one exported list, or in a small table the staff can edit.

How to check the fix. A search for the wrapper finds 1 copy, and the categories list is defined once.

16. A broken menu looks like an empty menu

SeverityLow
EffortS, about 2 hours
WhereThe function that loads the menu, and the homepage

What I found. When the menu query fails, the server logs the error and returns an empty list. The homepage then shows "No items in this category yet."

Why it matters. During a database problem, visitors see a cafe with nothing on the menu and leave. No one on the team is told.

How to fix it. Let the error reach the page, show "The menu did not load" with a retry button, and send the error to a log the owner sees.

How to check the fix. With a wrong database address in a test, the page shows the error and the retry button, not the empty message.

17. The settings file is in git

SeverityLow
EffortS, under 1 hour
Where.env, .gitignore and the README

What I found. .env is tracked in git although .gitignore lists it, and the README says only the example file is committed. Today it holds only the public Supabase keys and the project id, which are safe to share. I searched every commit: no real secret was ever in it.

Why it matters. The next person who adds a Stripe key or the service key there, trusting the README, publishes it in the git history for good.

How to fix it. git rm --cached .env and commit, so the file stays on disk but leaves git.

How to check the fix. git ls-files .env returns nothing.

18. Unused UI components

SeverityLow
EffortS, about 1 hour
WhereThe generated UI kit folder and package.json

What I found. The generated UI kit was added in full. 33 of its 46 files are not imported anywhere. 7 packages are used only by those files: cmdk, embla-carousel, input-otp, react-day-picker, react-resizable-panels, recharts and vaul.

Why it matters. The build drops unused code, so visitors do not pay for it. The team does: more files to read, and 7 more packages to keep updated and to check for advisories.

How to fix it. Delete the unused files and remove the 7 packages.

How to check the fix. The build still passes, and every page looks the same.

19. The menu reloads every 30 seconds

SeverityLow
EffortS, under 1 hour
WhereThe menu query on the homepage

What I found. The menu query uses refetchInterval: 30_000 and refetchOnWindowFocus: true.

Why it matters. Each homepage left open on screen calls the server and the database about 120 times an hour, and again every time the visitor switches back to the tab. The menu changes a few times a day at most. Small now. With traffic, it shows up on the hosting and database bill.

How to fix it. Reload every 5 minutes, or drop the timer and reload only when staff change the menu.

How to check the fix. With the page open for 10 minutes, the network tab shows 2 menu calls or fewer, not 20.

20. Loose types and a false "done" message

SeverityLow
EffortS, about 2 hours
WhereThe staff check, one staff action for reservations, and the staff dashboard

What I found. The database client is typed as any in 2 places, so the type checker cannot catch a wrong table or column name there. Every staff action checks the staff role first, except the one that confirms or cancels a reservation. For a signed-in user without the staff role, the database rule blocks the change correctly, but it returns no error, so the page shows a success message for a change that did not happen.

Why it matters. No data is at risk, since the database rule holds. It is a hidden source of type errors and a message that tells the truth only to staff.

How to fix it. Type the client with the generated database types, and add the same staff check to the reservation action.

How to check the fix. The type check passes with no any left in those places, and a non-staff user gets a refusal, not a success message.

21. Dollars are hard coded

SeverityLow
EffortS, about 2 hours
WhereThe price format in the cart and the Stripe Checkout setup

What I found. Prices are formatted with a $ sign, and Stripe is always asked for usd.

Why it matters. A cafe in the euro area, or anywhere outside the US, needs code changes before it can take a single payment in its own currency.

How to fix it. One currency setting, used both by Stripe and by a price format based on Intl.NumberFormat.

How to check the fix. Changing the setting to eur shows "€4.50" in the cart and charges in euros in Stripe test mode.

6. Dependency advisories

From the OSV.dev check of all 482 locked package versions in bun.lock. The first 4 rows reach the browser or the server. The rest run only on the developer's machine or during the build.

PackageLocked versionAdvisoryRatingUsed in
@tanstack/react-start1.168.32GHSA-qx66-fv34-fjm8, reflected cross-site scripting in server function responses, fixed in 1.168.60CriticalBrowser and server
@tanstack/start-server-core1.169.17GHSA-qx66-fv34-fjm8, same advisory, fixed in 1.169.39CriticalServer
seroval1.5.6GHSA-p6vx-979v-rg4c and GHSA-jp82-f5mq-hwhpCritical and HighBrowser and server
zod3.25.76GHSA-mrhx-c7xc-m3w5, memory useHighServer
vite 8.2.0, launch-editor, brace-expansion, js-yaml, nanoid, shell-quote, source-map-jsVariousAdvisories in build and dev toolsVariesDeveloper machine and build only

Updating TanStack Start (finding 3) moves most of these along with it.

7. What is done well

Worth saying, because it is why the verdict is fix and not rebuild.

WhatWhere
Row level security is on for all 5 tablesEvery migration
Prices and availability are always looked up on the server, never taken from the browserThe order function
The Stripe signature check uses a constant time compare and refuses events older than 5 minutesThe webhook
The private database key is used only in server codeThe server-only database client
Server functions are protected against cross-site requestsThe app's start-up settings
Database functions that ran with extra rights were removed againA later migration
Deleting a menu item used in past orders is blocked, with a clear messageThe menu editor
Forms keep what the user typed when sending failsThe cart and the table form
Strict TypeScript is ontsconfig.json
No real secret in any file or in any of the 50 commitsWhole repository and history

8. What I could not check

A sample uses the public code only. A paid audit closes these with the owner's access.

  1. The live database. Rules can be changed in the Supabase dashboard after the migrations, so the live rules may differ from the code. The auth settings (email confirmation, captcha, rate limits, leaked password protection) are unknown.
  2. Whether the demo admin login still works on the live app. Not tried on purpose: no login attempts on an app I do not own.
  3. Stripe. Whether keys are set, which payment methods are on, and whether Stripe Tax is set up (finding 7).
  4. Speed on a phone. Lighthouse and the real bundle size need a build.
  5. ESLint and type check results. Both need an install.
  6. Hosting. Lovable and Cloudflare settings, logs, backups and rollback.

9. Fix or rebuild

PartDecisionWhy
LoginFixSupabase login itself is sound. The demo credentials, the missing password reset and open sign-up are small changes.
Data (database rules)FixRules are on everywhere. 3 tables need the visitor insert right removed and writes moved to the server.
Ordering and paymentsFixThe flow is right (server pricing, signed webhook). It needs the paid check, repeat handling, one transaction and unique codes.
ReservationsFixWorks, but needs proper date, opening hours and capacity checks on the server.
Staff dashboard and menu editorKeepMenu and order actions check staff rights, input is validated, and deleting an item used in past orders is blocked. One small fix is in finding 20.
DesignKeepClean and responsive. Not part of the problem.

No part costs more than half of a rebuild to fix, so nothing is rebuilt.

10. Next step

Fixed prices, in order of severity.

StepWhatTimePrice
1. Critical fixesFindings 1, 2 and 4: demo login removed and password changed, visitor writes to orders, order items and reservations closed, writes moved to the server, checked against the live database.2 days750 EUR
2. Everything elseFindings 3 and 5 to 21: framework update, bot protection, payment fixes, transaction, order codes, reservation checks, password reset, deploy settings, tests for the money paths, cleanup.About 7 days2,250 EUR
Total3,000 EUR

Why the low end of the 3,000 to 8,000 EUR range: the app is small (about 4,300 lines of its own code), the structure is sound, and most fixes are small and local. Step 1 can start alone, so the 2 risks that hurt most are closed within the week.

Matjaž Korošec (koroCode)

← Back to the summary

Your app next?

Same report for your app in 5 days, from 1,500 EUR. You know the price before I start. Then you decide what gets fixed.

Your report stays private. Samples use public sites with names removed.