Skip to content
SealMyApp

Example report (demo app we own). This is real scanner output from a test app we built with deliberate mistakes. All its data is fake. We kept the 6 issues a typical Lovable + Supabase app might show and scored them with the same scoring as a real scan.

Security report · example

demo-app.example

Demo app built with Lovable and Supabase

Score out of 100.
Higher is better.

Critical

1

High

1

Medium

1

Low

3

Some results need your input. We found data that might be public on purpose. Where that's the case we ask a question rather than call it a problem.

The free scan shows you

  • Your score and how many issues of each severity
  • Your most serious issue in full (here: “Supabase service_role key exposed in frontend bundle”)
  • A plain-English summary of what's at stake

The Fix Pack (£29) adds

  • Every issue in full, like the list below
  • A copy-paste fix prompt for your builder, and Supabase SQL where needed
  • A re-scan to confirm each fix worked

What a Fix Pack looks like

6 issues · prompts for Lovable. We show two fix prompts in full here; a real Fix Pack has one for every issue.

  1. CriticalDatabase access (Supabase)

    Supabase service_role key exposed in frontend bundle

    Your Supabase service_role key is in the JavaScript sent to browsers. That key bypasses Row Level Security completely, so anyone can read, change or delete all your data.

    A service_role JWT eyJh…VyZQ (148 chars) was found in the bundle. This key bypasses all RLS and grants full database access.

    Do this first

    1. Rotate the key now: Supabase → Project Settings → API keys → roll the service_role / secret key. The old key is already public, so removing it from code is not enough.
    2. Put the new key only in server-side secrets (e.g. Supabase Edge Function secrets), never in a VITE_ / NEXT_PUBLIC_ variable.

    Then: fix prompt

    Paste this into the Lovable chat (Chat mode first if you want to review the plan):

    Remove the Supabase service_role key from all frontend code and environment variables exposed to the browser (anything prefixed VITE_, NEXT_PUBLIC_ or similar). The frontend must only use the anon/publishable key. Move any operation that genuinely needs elevated access into a server-side function (e.g. a Supabase Edge Function) that reads the service_role key from server-only secrets and checks the caller is allowed. List every file you changed.

    Check it worked

    • In Supabase → Project Settings → API keys, confirm the old service_role / secret key is no longer listed as active.
    • Redeploy, then re-scan with SealMyApp.
  2. HighDatabase access (Supabase)

    Supabase table "profiles" exposes personal data publicly (confirm this is intended)

    Anyone with your public Supabase anon key (which ships in every frontend) can read the "profiles" table, because Row Level Security is off or a policy allows anonymous reads. If this table is meant to be public, you can keep it readable — but check which columns are exposed.

    Anonymous SELECT on "profiles" returned 200 (1 row sampled, discarded). Columns: [id, email, full_name]. Personal-data columns: [email, full_name]. Fine if this table is meant to be public; otherwise add an RLS policy. No row values were stored.

    Question for you: Is the `profiles` table meant to be readable by anyone? It exposes columns: email, full_name.

    Fix prompt

    Paste this into the Lovable chat (Chat mode first if you want to review the plan):

    Enable Row Level Security on the Supabase table "profiles" and add policies so that only the signed-in owner of a row can read or change it (match rows on the column that stores the owner's user id against auth.uid()). Do not make any table public unless it holds intentionally public data. Show me the SQL migration before applying it, and update any queries that relied on anonymous access so they run as the signed-in user.

    SQL (Supabase → SQL editor)

    -- 0) First see which policies already exist. Policies are OR-ed together, so any existing
    --    "using (true)" or anon policy keeps the table open until you drop it (by its real name):
    --    select policyname, roles, cmd, qual from pg_policies where schemaname = 'public' and tablename = 'profiles';
    
    -- 1) Turn on Row Level Security. With RLS on and no policies, anonymous reads return nothing.
    alter table public."profiles" enable row level security;
    
    -- 2) Add only the access your app actually needs. EXAMPLE: rows owned by the signed-in user.
    --    ASSUMPTION: user_id stores the owner's auth.uid(). Replace it with your column (often "id" on profiles).
    drop policy if exists "Owners can read their rows" on public."profiles";
    create policy "Owners can read their rows"
      on public."profiles" for select
      to authenticated
      using ((select auth.uid()) = user_id);
    
    drop policy if exists "Owners can insert their rows" on public."profiles";
    create policy "Owners can insert their rows"
      on public."profiles" for insert
      to authenticated
      with check ((select auth.uid()) = user_id);
    
    drop policy if exists "Owners can update their rows" on public."profiles";
    create policy "Owners can update their rows"
      on public."profiles" for update
      to authenticated
      using ((select auth.uid()) = user_id)
      with check ((select auth.uid()) = user_id);
    
    drop policy if exists "Owners can delete their rows" on public."profiles";
    create policy "Owners can delete their rows"
      on public."profiles" for delete
      to authenticated
      using ((select auth.uid()) = user_id);
    
    -- If this table is genuinely meant to be public (e.g. published blog posts), use a read-only
    -- policy instead, scoped to what should be public:
    -- create policy "Public can read published rows" on public."profiles" for select to anon, authenticated using (published = true);

    Check it worked

    • In Supabase → Table Editor, "profiles" should show "RLS enabled".
    • Log out of your app and confirm the data no longer loads; log in and confirm your own rows still load.
    • Run a SealMyApp re-scan: this finding should disappear.
  3. MediumPublicly exposed file

    JavaScript source map is publicly accessible

    Source maps are publicly downloadable, which lets anyone read your original source code (including comments and internal endpoints).

    Source map https://demo-app.example/assets/app.js.map is served (200, valid v3 map). This can reveal original source, comments, and sometimes secrets.

    Fix prompt and checks for this issue are included in the Fix Pack.

  4. LowSecurity headers

    No Content-Security-Policy

    This is hardening rather than an open door, but it's quick to fix.

    The main page response has no Content-Security-Policy header or meta tag. A CSP limits the damage of any XSS bug.

    Fix prompt and checks for this issue are included in the Fix Pack.

  5. LowSecurity headers

    Page can be framed by other sites (no X-Frame-Options / frame-ancestors)

    This is hardening rather than an open door, but it's quick to fix.

    Neither X-Frame-Options nor a CSP frame-ancestors directive is set on the main page, so it can be embedded for clickjacking.

    Fix prompt and checks for this issue are included in the Fix Pack.

  6. LowDatabase access (Supabase)

    Supabase table "feedback" is publicly readable (confirm this is intended)

    Anyone with your public Supabase anon key (which ships in every frontend) can read the "feedback" table, because Row Level Security is off or a policy allows anonymous reads. If this table is meant to be public, you can keep it readable — but check which columns are exposed.

    Anonymous SELECT on "feedback" returned 200 (1 row sampled, discarded). Columns: [id, rating, comment]. No sensitive-looking columns. No row values were stored.

    Question for you: Is the `feedback` table meant to be readable by anyone? It exposes columns: id, rating, comment.

    Fix prompt and checks for this issue are included in the Fix Pack.

Then re-scan to prove it's fixed

After applying the fixes, you re-scan from your report. Here's the same demo app before and after we fixed it (both are real scans of our demo app).

Before

Score out of 100.
Higher is better.

6 issues

After re-scan

Score out of 100.
Higher is better.

No issues found by our checks

With no critical or high issues left, you can show a live “Sealed” badge. Watchdog (£9/month) then re-scans daily and emails you if a later AI edit re-opens something.

Notes (2)
  • Supabase anon key present (public by design)

    Anon key eyJh…VyZQ (138 chars) detected. This is expected in frontend code; what matters is whether RLS protects the data below.

  • Supabase table "products" is publicly readable (looks intentional)

    Anonymous SELECT on "products" returned 200 (1 row sampled, discarded). Columns: [id, name, price]. No sensitive-looking columns. No row values were stored.

What this scan can't see

We check your app from the outside, the way a stranger would: we never sign in, create accounts or change data. So we can't see problems that only appear once someone is logged in — for example, whether one signed-in user can read another user's data, or what anyone can do after an anonymous sign-in. A good score means our checks found nothing, not that your app is guaranteed secure.

Check your own app

Free, read-only, about two minutes. You'll prove the app is yours first: we never scan apps you don't own.

Scan my app free