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
| App | A 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. |
|---|---|
| Date | October 26, 2026 |
| Stack | React 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. |
| Size | About 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 used | The 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 used | git (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 why | npm 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 by | Matjaž 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
- 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.
- 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.
- 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.
- 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.
- 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
- Remove the demo login from the sign-in page and change the admin password.
- Stop visitors from writing to the orders, order items and reservations tables directly. Only the server should write there.
- 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.
| Area | What I checked | How |
|---|---|---|
| Secrets | API keys, Stripe keys, webhook secrets, database service keys, tokens, cloud keys | Searched 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 rules | Who can read, write, change and delete each of the 5 tables | Read all 9 migrations in order and worked out the final rules, since later migrations drop and replace earlier ones. |
| Login and roles | Sign-in, sign-up, password reset, who becomes staff | Read the sign-in page, the auth middleware, the staff check and the migration that hands out roles. |
| Money | Prices, totals, order saving, Stripe Checkout, the webhook, tax, currency | Followed one order from the cart to the kitchen screen, then one payment from Stripe back to the order. |
| Input | What the server accepts from the order and table forms | Read every validation schema on the server and compared it with the database checks and the forms. |
| Abuse | Rate limits, bot checks, sign-up controls | Searched the code for any limit, captcha or bot check on public writes. |
| Dependencies | Known security advisories in all 482 locked packages | Sent the exact versions from bun.lock to OSV.dev and read each advisory that came back. |
| Delivery | Settings file, build, tests, CI, rollback | Compared the example settings file with what the code reads, looked for tests, CI and a deploy plan. |
| Code health | Copies, dead code, loose types, running costs | Read 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.
| # | Where | Problem | What can happen | Severity | Fix | Effort |
|---|---|---|---|---|---|---|
| 1 | Staff sign-in page, database seed | The 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. | Critical | Remove the pre-filled values and the printed line, change the password today, check the login log for strangers. | S |
| 2 | Database rules, orders and order items tables | Visitors 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". | Critical | Let 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 |
| 3 | Dependencies | TanStack 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. | High | Update TanStack Start, seroval and zod, then click through the order, payment and staff flows. | S |
| 4 | Database rules, reservations table | Same 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. | High | Same change as finding 2, applied to reservations. | S |
| 5 | Order form, table request form, staff sign-up | No 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. | High | Add a bot check (for example Cloudflare Turnstile) and a per-visitor limit on the 3 server functions. | M |
| 6 | Payment webhook | The 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. | High | Mark paid only when the session's payment status is "paid", or on the "async payment succeeded" event. | S |
| 7 | Payment setup | Stripe 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. | Medium | Decide on tax with the owner, then either turn it off in code or show and save the tax. | S |
| 8 | Payment webhook | Every "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. | Medium | Only set the status when the order is not paid yet. Store the event id and ignore repeats. | S |
| 9 | Placing an order | The 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. | Medium | Save both in one database function, check every write. | M |
| 10 | Order codes | Pickup 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". | Medium | Add a unique rule and retry on a clash, or use a longer code. | S |
| 11 | Table request checks on the server | The 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. | Medium | Check date, opening hours and capacity on the server, from one shared list of hours. | M |
| 12 | Login | There 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. | Medium | Add "Forgot password", and turn off open sign-up (invite staff instead). | M |
| 13 | Deploy settings | The 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. | Medium | Fix the example file, add a build and test step before deploy, write down how to roll back. | S |
| 14 | Tests | No tests at all, and no test script. | Every change to ordering or payments is checked by hand, or by customers. | Medium | Add tests for the 3 money paths first: placing an order, the webhook, the signature check. | M |
| 15 | Code | The 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. | Medium | One shared helper, one categories list. | M |
| 16 | Menu loading | When 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. | Low | Show an error message with a retry button, and log it. | S |
| 17 | Repository | The .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. | Low | Remove it from git, keep only the example file. | S |
| 18 | Code | 33 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. | Low | Delete them. | S |
| 19 | Running costs | Every 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. | Low | Reload every few minutes, or only when the menu changes. | S |
| 20 | Types and checks | 2 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. | Low | Type the client, add the same staff check as the other actions. | S |
| 21 | Money format | Dollars and USD are hard coded in 2 files. | A cafe outside the US needs code changes to sell in its own currency. | Low | One 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
| Severity | Critical |
|---|---|
| Effort | S, under 2 hours plus the login log review |
| Where | The 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.
- Start both fields empty.
- Delete the "Demo access" line.
- Change the admin password in the Supabase dashboard today, before the code change goes live.
- 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"
| Severity | Critical |
|---|---|
| Effort | M, about 1 day |
| Where | The 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.
- 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.
- In a new migration, drop both insert rules and revoke insert rights on both tables from
anonandauthenticated. - 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
| Severity | High |
|---|---|
| Effort | S, half a day including a full click-through |
| Where | package.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.
- Update
@tanstack/react-startto at least1.168.60and let@tanstack/start-server-core,serovalandzodmove to their fixed versions. - 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
| Severity | High |
|---|---|
| Effort | S, done together with finding 2 |
| Where | The 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
| Severity | High |
|---|---|
| Effort | M, about 1 day |
| Where | The 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.
- 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.
- 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.
- 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
| Severity | High |
|---|---|
| Effort | S, about 2 hours |
| Where | The 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
| Severity | Medium |
|---|---|
| Effort | S, after a short decision with the owner |
| Where | The 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"
| Severity | Medium |
|---|---|
| Effort | S, about 2 hours |
| Where | The 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
| Severity | Medium |
|---|---|
| Effort | M, about 1 day, less if done together with finding 2 |
| Where | The 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
| Severity | Medium |
|---|---|
| Effort | S, about 2 hours |
| Where | The 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
| Severity | Medium |
|---|---|
| Effort | M, about 1 day |
| Where | The 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
| Severity | Medium |
|---|---|
| Effort | M, about 1 day |
| Where | The 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
| Severity | Medium |
|---|---|
| Effort | S, half a day |
| Where | The 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
| Severity | Medium |
|---|---|
| Effort | M, about 1.5 days for the 3 money paths |
| Where | The 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
| Severity | Medium |
|---|---|
| Effort | M, about 1 day |
| Where | 6 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
| Severity | Low |
|---|---|
| Effort | S, about 2 hours |
| Where | The 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
| Severity | Low |
|---|---|
| Effort | S, 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
| Severity | Low |
|---|---|
| Effort | S, about 1 hour |
| Where | The 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
| Severity | Low |
|---|---|
| Effort | S, under 1 hour |
| Where | The 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
| Severity | Low |
|---|---|
| Effort | S, about 2 hours |
| Where | The 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
| Severity | Low |
|---|---|
| Effort | S, about 2 hours |
| Where | The 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.
| Package | Locked version | Advisory | Rating | Used in |
|---|---|---|---|---|
@tanstack/react-start | 1.168.32 | GHSA-qx66-fv34-fjm8, reflected cross-site scripting in server function responses, fixed in 1.168.60 | Critical | Browser and server |
@tanstack/start-server-core | 1.169.17 | GHSA-qx66-fv34-fjm8, same advisory, fixed in 1.169.39 | Critical | Server |
seroval | 1.5.6 | GHSA-p6vx-979v-rg4c and GHSA-jp82-f5mq-hwhp | Critical and High | Browser and server |
zod | 3.25.76 | GHSA-mrhx-c7xc-m3w5, memory use | High | Server |
vite 8.2.0, launch-editor, brace-expansion, js-yaml, nanoid, shell-quote, source-map-js | Various | Advisories in build and dev tools | Varies | Developer 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.
| What | Where |
|---|---|
| Row level security is on for all 5 tables | Every migration |
| Prices and availability are always looked up on the server, never taken from the browser | The order function |
| The Stripe signature check uses a constant time compare and refuses events older than 5 minutes | The webhook |
| The private database key is used only in server code | The server-only database client |
| Server functions are protected against cross-site requests | The app's start-up settings |
| Database functions that ran with extra rights were removed again | A later migration |
| Deleting a menu item used in past orders is blocked, with a clear message | The menu editor |
| Forms keep what the user typed when sending fails | The cart and the table form |
| Strict TypeScript is on | tsconfig.json |
| No real secret in any file or in any of the 50 commits | Whole 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.
- 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.
- 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.
- Stripe. Whether keys are set, which payment methods are on, and whether Stripe Tax is set up (finding 7).
- Speed on a phone. Lighthouse and the real bundle size need a build.
- ESLint and type check results. Both need an install.
- Hosting. Lovable and Cloudflare settings, logs, backups and rollback.
9. Fix or rebuild
| Part | Decision | Why |
|---|---|---|
| Login | Fix | Supabase login itself is sound. The demo credentials, the missing password reset and open sign-up are small changes. |
| Data (database rules) | Fix | Rules are on everywhere. 3 tables need the visitor insert right removed and writes moved to the server. |
| Ordering and payments | Fix | The flow is right (server pricing, signed webhook). It needs the paid check, repeat handling, one transaction and unique codes. |
| Reservations | Fix | Works, but needs proper date, opening hours and capacity checks on the server. |
| Staff dashboard and menu editor | Keep | Menu 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. |
| Design | Keep | Clean 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.
| Step | What | Time | Price |
|---|---|---|---|
| 1. Critical fixes | Findings 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 days | 750 EUR |
| 2. Everything else | Findings 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 days | 2,250 EUR |
| Total | 3,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)
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.