← App check

Sample report. The app, the data and the findings are made up. Your real report sits in your client area, with its own login.

Sample report

App check report.
October 2026.

No function in the sample.
App
Appointment booking for physiotherapy practices
Built with
Lovable, Supabase, Stripe, Twilio
Size
14 tables, 6 edge functions, 22 pages, 3 roles
Reviewed state
Commit a3f9c21 from 2 October 2026
Review time
5 business days
Verdict

Not ready for real patient data yet. But close.

Five critical findings in three areas, all fixable in a few days. Ten more should be done before launch, five can wait until after. The foundation holds, no rebuild needed.

  • 5

    Critical

  • 10

    Fix before launch

  • 5

    After launch

Method

How it was checked. And what was not.

Code review
Read the whole repository: frontend, six edge functions and all migrations.
Database
Compared schema, policies and functions in the live project with the migrations. There were no differences.
Test accounts
One account per role (patient, therapist, practice admin) on a copy of the project. With each one, deliberately tried to read and change other people's data.
Shipped code
Searched the JavaScript that ends up in the browser for keys, secrets and internal addresses.
Tools
npm audit, the Supabase security advisor and my own scripts that test every policy with every role.
Configuration
The settings of Supabase Auth, Storage, Stripe and Twilio, with read access.
Not checked
  • No penetration test: no attacks on the live system from the outside.
  • No load or performance tests.
  • No legal assessment.
  • Not the infrastructure of Lovable, Supabase, Stripe and Twilio themselves.
  • No accessibility review. That is part of production readiness.
Severity
  • Critical

    Data or money is at risk right now. Fix immediately, even if the app is already live.

  • Fix before launch

    Becomes a risk as soon as real users arrive. Fix before launch.

  • After launch

    No acute risk. Improves maintainability or quality.

Findings

What was found. And what to do.

01Auth & permissions

Critical
1.1Critical

Role can be changed by the user

Finding
The role lives in the profiles table, column role. The update policy lets every user change their own row in full, the role included.
Why it is risky
A patient can make themselves practice admin with a single request from the browser. After that they see and change every appointment, patient and invoice of the practice.
Where
supabase/migrations/20260512_profiles.sql, policy “Users can update own profile”
Recommendation
  1. Take the role column out of the update policy. Users may only change name, phone and notifications.
  2. Assign roles only through an edge function that checks whether the caller is practice admin.
  3. Add a test that tries to change its own role and has to fail.
1.2Fix before launch

Admins without two-factor login

Finding
Practice admins and therapists log in with email and password only. Supabase offers a second step via an authenticator app, and it is not enabled.
Why it is risky
These accounts see the data of every patient of a practice. A guessed or leaked password is enough to take them over.
Where
Supabase Auth, multi-factor authentication settings
Recommendation
  1. Enable login via authenticator app (TOTP) in Supabase.
  2. Make the second step mandatory for therapists and practice admins and check it in the policies (aal2).
  3. Define a process for lost devices, for example recovery by the practice admin.
1.3Fix before launch

Weak password rules

Finding
Passwords only need six characters. The check against known leaked passwords is switched off.
Why it is risky
Short and known passwords can be guessed quickly.
Where
Supabase Auth, password settings
Recommendation
  1. Set the minimum length to ten characters.
  2. Turn on the check against leaked passwords.
  3. Update the hint in the sign-up form.
What is fine
Login via Supabase Auth with email confirmation. Password reset is set up correctly, sessions expire after seven days.

02Database policies

Critical
2.1Critical

Appointments readable by everyone

Finding
Row level security is on for the appointments table. But the select policy only checks whether someone is logged in: using (true) for the authenticated role.
Why it is risky
Every registered patient can fetch all appointments, with names, phone numbers and the therapists' notes. That is health data. A leak here has to be reported.
Where
Table appointments, policy “Enable read access for authenticated users”
Recommendation
  1. Patients only see appointments with patient_id = auth.uid().
  2. Therapists only see appointments of their practice, checked via the practice_members table.
  3. Move the notes into their own table that only therapists can read.
  4. For every role, a test that queries someone else's appointments and has to get nothing back.
2.2Critical

Prescriptions publicly downloadable

Finding
Patients upload doctor's prescriptions as PDF. The documents storage bucket is public, and files sit under patient ID and file name.
Why it is risky
Anyone who knows or guesses the link downloads the file without logging in. Prescriptions contain diagnoses, and links quickly end up in emails, chats and browser histories.
Where
Supabase Storage, bucket documents
Recommendation
  1. Make the bucket private.
  2. Control access via storage policies: the patient and the therapists of their practice.
  3. Serve files only through short-lived signed links, for example 60 seconds.
  4. Check whether links have already been shared, and rename the files if needed.
2.3Fix before launch

Statistics function without a check

Finding
The database function get_practice_stats runs as SECURITY DEFINER, so with full rights. It takes a practice ID and does not check whether the caller belongs to that practice.
Why it is risky
Every logged-in user can query revenue and appointment numbers of every practice. That is business data of your customers.
Where
supabase/migrations/20260603_stats.sql, function get_practice_stats
Recommendation
  1. At the start of the function, check whether auth.uid() is admin of the requested practice.
  2. Revoke the function from the anon role.
  3. The two other SECURITY DEFINER functions are fine. Secure new functions the same way.
What is fine
All 14 tables have row level security on. The invoices and practices tables are correctly limited to the user's own practice.

03API keys & secrets

Critical
3.1Critical

Stripe secret key exposed

Finding
The key sits in the environment variable VITE_STRIPE_SECRET_KEY. Everything with the VITE_ prefix ends up in the JavaScript that is shipped to the browser.
Why it is risky
Anyone who opens the page can read the key. With it, payments can be refunded, customer data pulled from Stripe and payouts viewed.
Where
src/lib/stripe.ts, line 4
Recommendation
  1. Roll the key in Stripe right away. Treat the old one as known.
  2. Store the new key only as a Supabase secret and handle payments through an edge function.
  3. Use only the publishable key in the browser.
  4. Go through the other VITE_ variables. All of them are public.
3.2Critical

Stripe webhook without signature check

Finding
The stripe-webhook edge function marks appointments as paid as soon as a checkout.session.completed message arrives. It does not check whether the message really comes from Stripe.
Why it is risky
Anyone can send such a message themselves and mark appointments as paid without paying.
Where
supabase/functions/stripe-webhook/index.ts
Recommendation
  1. Store the webhook secret from Stripe as a Supabase secret.
  2. Verify every message with stripe.webhooks.constructEvent and reject invalid ones with status 400.
  3. Also check the payment status with Stripe before an appointment counts as paid.
3.3After launch

Old Twilio token in the Git history

Finding
A commit from March contains a Twilio token in plain text. The token is no longer valid.
Why it is risky
No risk right now. But it shows that secrets have ended up in the code before.
Where
Git history, commit 4e1b07d, file src/lib/sms.ts
Recommendation
  1. Confirm in Twilio that the token really is revoked.
  2. Turn on secret scanning in the repository, so new cases show up right away.
What is fine
The Supabase service role key is only used in edge functions. The .env files are excluded from Git.

04Input validation

Fix before launch
4.1Fix before launch

Input only checked in the browser

Finding
The booking form checks input in the browser only. The create-booking edge function takes it as it comes. The “reason for visit” field has no length limit, the phone number no format.
Why it is risky
Anyone who sends the request directly skips the check. Very long texts bloat the database, invalid numbers make SMS reminders go nowhere.
Where
supabase/functions/create-booking/index.ts
Recommendation
  1. One shared zod schema for the browser and the edge function.
  2. Limit the reason for visit to 1,000 characters, check phone numbers in international format.
  3. Reject invalid requests with a clear error message.
4.2Fix before launch

Uploads without type and size check

Finding
The prescription upload accepts every file type up to 50 MB, HTML and SVG included.
Why it is risky
An uploaded HTML file can serve a convincing fake page under your storage address. Large files drive up storage costs.
Where
src/pages/booking/UploadStep.tsx, bucket documents
Recommendation
  1. Allow only PDF, JPG and PNG, in the browser and in the bucket configuration.
  2. Limit the size to 10 MB.
  3. Always serve files as downloads, never display them directly in the browser.
4.3After launch

Error messages reveal database details

Finding
When a booking fails, the app shows the raw database error, with table and column names.
Why it is risky
No direct risk. But attackers learn how the database is structured, and users do not understand the messages.
Where
src/lib/api.ts, function handleError
Recommendation
  1. Replace errors in the browser with clear messages.
  2. Keep the details in error tracking only.
What is fine
Prices are calculated on the server, not taken from the browser. Text is escaped correctly when displayed.

05Dependencies

Fine
5.1After launch

Two packages outdated

Finding
date-fns and react-day-picker are one major version behind.
Why it is risky
No acute risk. The longer updates wait, the bigger they get.
Where
package.json
Recommendation
  1. Update both packages when convenient and test the calendar view.
  2. Turn on automatic update alerts, for example with Dependabot.
5.2After launch

Unused package in the bundle

Finding
moment.js is installed and imported in one file, but no longer used.
Why it is risky
No risk. But it makes the app about 70 KB bigger and slower on first load.
Where
src/components/LegacyCalendar.tsx
Recommendation
  1. Remove the import and the package.
What is fine
npm audit reports no known vulnerabilities. The lockfile is in the repository.

06Cost & abuse

Fix before launch
6.1Fix before launch

SMS sending without a limit

Finding
The send-reminder edge function sends SMS via Twilio. Every logged-in user can call it, and there is no limit.
Why it is risky
A single script can trigger thousands of messages. The bill goes to you, each SMS costs about 8 cents.
Where
supabase/functions/send-reminder/index.ts
Recommendation
  1. Call the function from the server only, for example via cron, not from the browser.
  2. Add a limit per practice and day.
6.2Fix before launch

Unlimited reservations

Finding
One account can reserve any number of appointments at once. Unpaid reservations stay for 24 hours.
Why it is risky
A few accounts can block a practice's calendar for a whole day. Real patients then find no free slot.
Where
supabase/functions/create-booking/index.ts
Recommendation
  1. At most three open reservations per account.
  2. Release unpaid reservations after 30 minutes.
  3. Flag suspicious accounts in the admin area.
6.3After launch

No cost alerts

Finding
There are no alerts for unusual costs in Supabase and Twilio.
Why it is risky
Abuse only shows up with the monthly bill.
Where
Billing settings of Supabase and Twilio
Recommendation
  1. Keep the spend cap on in Supabase and set up email alerts.
  2. Set a spending limit in Twilio and an alert from €50 per day.
What is fine
Sign-up with email confirmation and captcha. The Supabase Auth rate limits are on.

07GDPR basics

Fix before launch
7.1Fix before launch

Data stored in the US

Finding
The Supabase project runs in the us-east-1 region. There are no data processing agreements on file for Supabase, Stripe and Twilio.
Why it is risky
Appointments and notes are health data, and stricter rules apply to them. An EU region is the simpler way.
Where
Supabase project settings
Recommendation
  1. Create a new project in the Frankfurt region (eu-central-1) and move the data.
  2. Sign data processing agreements with Supabase, Stripe and Twilio and file them.
  3. Update the privacy policy to the new location.
7.2Fix before launch

Email addresses in logs

Finding
The edge functions write the patient's email address into the logs with every booking.
Why it is risky
Personal data in logs is often forgotten when someone asks for access or deletion.
Where
supabase/functions/*/index.ts
Recommendation
  1. Log IDs only, no email addresses or names.
  2. Limit log retention to seven days.
7.3Fix before launch

Incomplete deletion

Finding
When a patient deletes their account, their appointments, notes and uploaded prescriptions remain.
Why it is risky
Anyone who asks for their data to be deleted has to get that, as long as no retention obligation applies.
Where
supabase/functions/delete-account/index.ts
Recommendation
  1. Decide which data is deleted and which is only locked because of retention obligations.
  2. Anonymize appointments, delete or lock notes and files.
  3. Cover the deletion with a test.
What is fine
There is a privacy policy, no tracking cookies, and accounts can be deleted in the app.

Technical assessment, not legal advice. Please clarify retention obligations with your lawyer.

Offer

Production readiness. At a fixed price.

€7,400

Fixed price · 3 weeks

All 20 findings in this report, plus tests, CI/CD, monitoring, error tracking and backups. No rebuild: your app stays your app.

  • Week 1

    Critical gaps

    Roles, appointments, prescriptions, the Stripe key and the webhook. After that, no patient data is exposed, and nobody books without paying.

    €2,900

  • Week 2

    Operations

    Two-factor login, input and uploads, limits for SMS and reservations. Plus tests, CI/CD, monitoring, error tracking and backups.

    €2,600

  • Week 3

    Data protection & handover

    Move to Frankfurt, clean logs, complete deletion, the remaining points, documentation and handover.

    €1,900

  • Each milestone half at the start and half after acceptance. You have five business days for acceptance.
  • The check cost €1,290. Half of it, €645, is credited on the first invoice.
  • You can also implement the report yourself, with your team or with your AI tools. The offer is not an obligation. Afterwards I re-check on request, at half the check fee.
Your report

This is what your report looks like.
Just with your app.

After five business days the report is in your client area, plus a 30-minute review call. Fixed price from €990, I tell you the exact price up front.