This is a real report on SnapBook, a demo app we built with the most common AI-builder mistakes on purpose. Your report follows the same format.

Launch Check report: SnapBook (sample)

Prepared for: SnapBook (our own demo app, built with these flaws on purpose.)
Prepared by: Tidyship
Date: 27 October 2026. Code reviewed: the SnapBook demo code
Scope: the four launch risks below, reviewed from the code and database migrations. This is a point-in-time review, not a full penetration test or a guarantee of security.


Summary

Area Rating In one line
1. Can strangers see or change your users' data? 🔴 Every booking, with customer names and phone numbers, can be read and edited by anyone.
2. Are secret keys exposed in your website? 🔴 The Supabase "service role" master key is bundled into the website.
3. Can people get paid features without paying? 🔴 Pro unlocks by visiting /success, and the Stripe webhook accepts fake events.
4. Can anyone open your admin pages? 🔴 /admin is hidden from the menu but opens for anyone, showing every customer's booking.
  • 🔴 Fix before launch. Real users or money are at risk.
  • 🟠 Fix soon. It's a weakness, but not exposed right now, or exploiting it takes effort.
  • 🟢 Looks good. Nothing found in this area.

Bottom line: SnapBook isn't safe to launch yet. Anyone could read or change every customer's bookings and get Pro for free. All of it is fixable in about a day. Start with finding 1, the master key in the website. Until that key is removed and replaced, none of the other fixes protect anything.


Findings

1. The master database key is inside your website. 🔴

What we found: The app uses Supabase's service role key in the browser. That key ignores every access rule, and because it's in a VITE_ setting, it's copied into the website code that every visitor downloads.

Why it matters: Anyone who opens the browser's developer tools can copy it and gain full control of your database: read, change or delete everything.

Where: src/lib/supabase.ts, .env.example (VITE_SUPABASE_SERVICE_ROLE_KEY)

How to fix it:

  • If you use Lovable or Bolt, paste this prompt:

    "Remove every use of the Supabase service role key from the frontend. The browser client must only use the anon (publishable) key. Move any query that needs elevated access into a Supabase Edge Function. Remove VITE_SUPABASE_SERVICE_ROLE_KEY from the environment."

  • If a developer is fixing it: use VITE_SUPABASE_ANON_KEY only in createClient. Then rotate the service role key in Supabase (Project Settings, then API), because the old one must be treated as leaked.

How to check it's fixed: search the built site (dist/) for service_role. Nothing should come up. In Supabase, the old key should show as rotated.


2. Anyone can read and edit every booking. 🔴

What we found: Row Level Security, the database's per-user access rules, was never switched on for the bookings table.

Why it matters: Using only the public key that's meant to be in the website, anyone can list every booking, including customer names and phone numbers, and change or delete them. That's a data breach, and for EU or UK customers it's a reportable one.

Where: supabase/migrations/0001_init.sql, table bookings

How to fix it:

  • If you use Lovable or Bolt, paste this prompt:

    "Enable Row Level Security on the bookings table. Add policies so users can only select, insert, update and delete bookings where user_id equals their own auth.uid(). Admins (profiles.role = 'admin') may select all bookings. Don't change any other tables."

  • If a developer is fixing it:
    alter table public.bookings enable row level security;
    create policy "own bookings: read"   on public.bookings for select using (auth.uid() = user_id);
    create policy "own bookings: add"    on public.bookings for insert with check (auth.uid() = user_id);
    create policy "own bookings: change" on public.bookings for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
    create policy "own bookings: remove" on public.bookings for delete using (auth.uid() = user_id);
    

How to check it's fixed: in Supabase, open the Table editor. bookings should show "RLS enabled", and the Security Advisor should no longer list it.


3. Every user's profile and phone number is public, and users can make themselves Pro or admin. 🔴

What we found: The profiles table lets anyone read every profile (using (true)). It also lets users update their whole profile row, including the is_pro and role columns.

Why it matters: Personal data is exposed to the internet. Any signed-in user can switch on Pro, or make themselves an admin, with one request.

Where: supabase/migrations/0001_init.sql, policies on profiles

How to fix it:

  • If you use Lovable or Bolt, paste this prompt:

    "On the profiles table, replace the public select policy with one that only lets users read their own profile. Users may update only full_name and phone on their own row. is_pro and role must only be changed by the server, never from the browser."

  • If a developer is fixing it:
    drop policy "Profiles are viewable" on public.profiles;
    create policy "own profile: read" on public.profiles for select using (auth.uid() = id);
    revoke update on public.profiles from anon, authenticated;
    grant update (full_name, phone) on public.profiles to authenticated;
    

How to check it's fixed: while signed in as a normal user, try to change is_pro from the app. It should fail with a permission error.


4. Pro unlocks without paying. 🔴

What we found: Two problems. First, the "thank you" page (/success) sets the user to Pro when it loads, so typing that address unlocks Pro for free. Second, the Stripe webhook accepts any message without checking that it really came from Stripe.

Why it matters: This is lost revenue. Anyone can get Pro without paying, and anyone who finds the webhook address can grant Pro to any account.

Where: src/pages/Success.tsx, supabase/functions/stripe-webhook/index.ts

How to fix it:

  • If you use Lovable or Bolt, paste this prompt:

    "Remove the code on the success page that sets is_pro. Only the Stripe webhook Edge Function may set is_pro, after verifying the Stripe signature with the webhook signing secret. Store each Stripe event ID in stripe_events and skip events already processed."

  • If a developer is fixing it: verify the signature in the Edge Function, using the raw body:
    import Stripe from "npm:stripe@14";
    const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
    const crypto = Stripe.createSubtleCryptoProvider();
    const body = await req.text();
    const event = await stripe.webhooks.constructEventAsync(
      body, req.headers.get("Stripe-Signature")!,
      Deno.env.get("STRIPE_WEBHOOK_SIGNING_SECRET")!, undefined, crypto,
    );
    // then: insert event.id into stripe_events (skip if it already exists) before granting Pro
    

How to check it's fixed: visiting /success directly no longer changes your plan. In Stripe, Developers, then Webhooks, test events still go through, but a request without a valid signature gets an error.


5. The admin page isn't locked. 🔴

What we found: The Admin link is hidden from the menu unless the browser says you're an admin, but the browser can be told anything. The /admin page itself has no check, and it loads every booking.

Why it matters: Anyone who guesses /admin sees every customer's details. Fixes 1 and 2 stop the data loading, but the page should also refuse non-admins.

Where: src/App.tsx, src/pages/Admin.tsx

How to fix it:

  • If you use Lovable or Bolt, paste this prompt:

    "Protect the /admin route. Read the signed-in user's role from the profiles table (not localStorage), redirect non-admins away, and make sure all admin data comes from queries that the database only allows for admins."

  • If a developer is fixing it: add a database-side check, so the data is safe even if the page loads:
    create function public.is_admin() returns boolean
      language sql stable security definer set search_path = ''
      as $$ select exists (select 1 from public.profiles where id = auth.uid() and role = 'admin') $$;
    create policy "admins: read all bookings" on public.bookings for select using (public.is_admin());
    

How to check it's fixed: sign in as a normal user and open /admin. You should be redirected, and no bookings should load.


What we checked and found fine

  • The Stripe secret key is only used server-side (no VITE_ prefix).
  • No real .env file is committed. Only .env.example, with placeholders.
  • Prices come from a Stripe Price ID, not an amount set in the browser.

Recommended next steps

  1. Remove and rotate the service role key (finding 1). Do this today.
  2. Switch on RLS for bookings and tighten profiles (findings 2 and 3).
  3. Move the Pro upgrade to the verified webhook (finding 4).
  4. Lock /admin (finding 5).
  5. Optional: Fix sprint. We fix every red and amber item, add tests around sign-in and payments, and re-check. Fixed price from $1,500, about 1 week.

Housekeeping

  • For a real client, this section reminds them to remove our access and confirms we've deleted our copy of their code.

Want this for your app?

Fixed price, plain English, delivered in 3 days.

Get a Launch Check, from $125