<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[VibeVigil]]></title><description><![CDATA[VibeVigil]]></description><link>https://vibevigil.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 12 Nov 2026 07:22:44 GMT</lastBuildDate><atom:link href="https://vibevigil.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Check if Your Vibe-Coded App Is Leaking API Keys]]></title><description><![CDATA[A plain-English checklist to find secret API keys hiding in your Lovable, Bolt, v0, or Cursor app: in the browser code, in .env files, in source maps, and…]]></description><link>https://vibevigil.hashnode.dev/how-to-check-if-your-vibe-coded-app-is-leaking-api-keys</link><guid isPermaLink="true">https://vibevigil.hashnode.dev/how-to-check-if-your-vibe-coded-app-is-leaking-api-keys</guid><category><![CDATA[Security]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><category><![CDATA[supabase]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[VibeVigil]]></dc:creator><pubDate>Wed, 11 Nov 2026 03:39:19 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Not sure if your app is leaking keys? Run a free scan first.</strong> VibeVigil checks your live app for exposed keys, Supabase tables without RLS, missing security headers, SSL problems and downtime. Every issue comes with a ready-to-paste prompt for your coding agent. <a href="https://vibevigil.com/#scan"><strong>Scan my app for free →</strong></a> (only scan apps you own)</p>
</blockquote>
<h1>How to Check if Your Vibe-Coded App Is Leaking API Keys</h1>
<p>AI coding tools are great at making things work. Sometimes "working" means a secret key got pasted into code that runs in the browser. If that happens, anyone who opens your site can copy the key and use it. They can run up your AI bill, read your database, or send email as you.</p>
<p>This guide shows you how to check your own app in about 20 minutes. You don't need to be a developer.</p>
<h2>The short answer</h2>
<ol>
<li>Open your live app, press <strong>F12</strong> (or right-click and pick <strong>Inspect</strong>), and go to the <strong>Sources</strong> or <strong>Network</strong> tab.</li>
<li>Search all files for these words: <code>sk_</code>, <code>sk-</code>, <code>sb_secret_</code>, <code>service_role</code>, <code>OPENAI</code>, <code>ANTHROPIC</code>, <code>STRIPE</code>, <code>RESEND</code>, <code>secret</code>.</li>
<li>Anything that looks like a real secret key is a leak. Rotate that key today (see "If you find a leaked key" below).</li>
</ol>
<p>Some keys are <strong>meant</strong> to be public. The next section shows you which ones.</p>
<h2>Public keys vs. secret keys</h2>
<p>Not every key in your code is a problem. Here is a quick guide:</p>
<table>
<thead>
<tr>
<th>Key</th>
<th>Safe in the browser?</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td>Supabase publishable key (<code>sb_publishable_...</code>) or legacy <code>anon</code> key</td>
<td>Yes</td>
<td>It only reaches what your Row Level Security (RLS) rules allow.</td>
</tr>
<tr>
<td>Supabase secret key (<code>sb_secret_...</code>) or legacy <code>service_role</code> key</td>
<td><strong>No</strong></td>
<td>It skips every RLS rule. It is the master key to your data.</td>
</tr>
<tr>
<td>Stripe publishable key (<code>pk_live_...</code>)</td>
<td>Yes</td>
<td>It can't move money on its own.</td>
</tr>
<tr>
<td>Stripe secret key (<code>sk_live_...</code>) or restricted key (<code>rk_live_...</code>)</td>
<td><strong>No</strong></td>
<td>It can read customers and make charges and refunds.</td>
</tr>
<tr>
<td>OpenAI, Anthropic, Gemini and other AI keys</td>
<td><strong>No</strong></td>
<td>Anyone can use them and you pay the bill.</td>
</tr>
<tr>
<td>Email keys (Resend, SendGrid, Postmark)</td>
<td><strong>No</strong></td>
<td>Someone could send spam or phishing from your domain.</td>
</tr>
</tbody></table>
<p>Supabase's own docs put it simply: the publishable key is "safe to expose online," but secret keys "provide full access to your project's data, bypassing Row Level Security." (<a href="https://supabase.com/docs/guides/api/api-keys">Supabase: API keys</a>)</p>
<p><strong>Heads up:</strong> Supabase says it is deprecating the old <code>anon</code> and <code>service_role</code> keys by the end of 2026, and moving to publishable (<code>sb_publishable_</code>) and secret (<code>sb_secret_</code>) keys. If an AI tool tells you to paste a long key that starts with <code>eyJ</code>, it is using the old style. (<a href="https://supabase.com/docs/guides/api/api-keys">Supabase: API keys</a>)</p>
<h2>Step 1: Search the code your visitors download</h2>
<p>Everything in the browser is public. Check it like a stranger would.</p>
<ol>
<li>Open your live site in Chrome or Edge.</li>
<li>Press <strong>F12</strong> and open the <strong>Sources</strong> tab.</li>
<li>Press <strong>Ctrl+Shift+F</strong> (Windows) or <strong>Cmd+Option+F</strong> (Mac) to search across all files.</li>
<li>Search for each of these, one at a time:</li>
</ol>
<pre><code class="language-text">sk_live    sk_test    rk_live    sk-    sb_secret_    service_role
OPENAI     ANTHROPIC  GEMINI     RESEND    SENDGRID    secret    password
</code></pre>
<ol start="5">
<li>Also open the <strong>Network</strong> tab, reload the page, and click around your app. Look at the request headers. A secret key should never be sent from the browser.</li>
</ol>
<p>If you find <code>service_role</code> inside a long <code>eyJ...</code> key, you can paste that key into a JWT decoder you trust offline. The <code>role</code> field will say <code>anon</code> (fine) or <code>service_role</code> (leak).</p>
<h2>Step 2: Check your environment variable names</h2>
<p>Most vibe-coded apps use Vite (Lovable, Bolt) or Next.js (v0, many Cursor apps). Both tools copy certain variables <strong>into the browser code on purpose</strong>:</p>
<ul>
<li>In <strong>Vite</strong>, any variable that starts with <code>VITE_</code> ends up in the browser. (<a href="https://vite.dev/guide/env-and-mode">Vite: Env Variables and Modes</a>)</li>
<li>In <strong>Next.js</strong>, any variable that starts with <code>NEXT_PUBLIC_</code> ends up in the browser. (<a href="https://nextjs.org/docs/app/guides/environment-variables">Next.js: Environment Variables</a>)</li>
</ul>
<p>So open your <code>.env</code> file (or your host's environment settings) and look for names like these:</p>
<pre><code class="language-text">VITE_OPENAI_API_KEY          &lt;- leak
NEXT_PUBLIC_STRIPE_SECRET    &lt;- leak
VITE_SUPABASE_SERVICE_ROLE   &lt;- leak
VITE_SUPABASE_PUBLISHABLE_KEY  &lt;- fine
</code></pre>
<p>Rule of thumb: if a variable has a public prefix, it must hold only a public key.</p>
<h2>Step 3: Make sure your .env file isn't public</h2>
<p>Some hosts serve every file in your project folder. Try opening these in your browser, using your own domain:</p>
<pre><code class="language-text">https://your-app.com/.env
https://your-app.com/.env.local
https://your-app.com/.git/config
</code></pre>
<p>You should get a "not found" page or your normal app page. If you see text with keys or settings, the file is public. Rotate every key in it and block the file on your host.</p>
<h2>Step 4: Check source maps</h2>
<p>Source maps are files that turn your shrunk-down code back into readable code. They make debugging easy, but they also show your original code to anyone.</p>
<p>In the <strong>Sources</strong> tab, look for your original file names (like <code>src/lib/openai.ts</code>). If you can read them, your source maps are public. That's not always a disaster, but any secret in your source code is now easy to find. Turn source maps off for production unless you need them.</p>
<h2>Step 5: Check your GitHub history</h2>
<p>If your app syncs to GitHub (Lovable, Bolt and Cursor all can), a key might live in an old commit, even if the file is gone now. Deleting a file does not remove it from history. (<a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository">GitHub: Removing sensitive data from a repository</a>)</p>
<p>If you are comfortable with a terminal, run this in your project folder:</p>
<pre><code class="language-bash">git log -p -S "sk_live" --all
git log -p -S "service_role" --all
git log -p -S "OPENAI_API_KEY" --all
</code></pre>
<p>Any result means the key was committed at some point. Treat it as leaked, especially if the repo is public or was ever public. Also make sure <code>.env</code> is listed in your <code>.gitignore</code> file.</p>
<p>GitHub has built-in secret scanning and push protection that can catch many keys before they land. Turn it on in your repo's <strong>Settings → Code security</strong>. (<a href="https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning">GitHub: About secret scanning</a>)</p>
<h2>If you find a leaked key</h2>
<p>Do these in order:</p>
<ol>
<li><strong>Make a new key</strong> in that service's dashboard (Supabase, Stripe, OpenAI and so on).</li>
<li><strong>Move the new key to the server.</strong> Put it in a backend function, like a Supabase Edge Function, a Next.js server route, or your host's server environment. Never give it a <code>VITE_</code> or <code>NEXT_PUBLIC_</code> name.</li>
<li><strong>Update your app</strong> so the browser calls your server function, and your server function calls the paid service.</li>
<li><strong>Turn off the old key</strong> once everything works with the new one. For Supabase, you make a new secret key, swap it in, then delete or deactivate the old one. (<a href="https://supabase.com/docs/guides/api/api-keys">Supabase: Rotate a leaked key</a>)</li>
<li><strong>Check the bill and the logs</strong> for the service. Look for use you don't recognize. For AI keys, set a monthly spending limit if the provider offers one.</li>
</ol>
<p>Removing the key from your code is not enough on its own. Anyone who copied it can still use it until you turn it off.</p>
<h2>Paste this prompt into your coding agent</h2>
<p>Works in Lovable, Bolt, v0, Cursor or Claude Code:</p>
<pre><code class="language-text">Audit my app for leaked API keys and fix it. Don't delete any data.

1. Search the whole project, including .env files and config, for secret keys:
   Stripe sk_/rk_, Supabase service_role or sb_secret_, OpenAI, Anthropic, Gemini,
   Resend, SendGrid, and anything named SECRET, TOKEN or PASSWORD.
2. List every environment variable that starts with VITE_ or NEXT_PUBLIC_ and tell me
   which ones hold a secret. Only public keys may use those prefixes.
3. Move every secret key to server-side code (a Supabase Edge Function or a server route).
   The browser must call my server, and only the server may call the paid API.
4. Make sure the frontend uses only the Supabase publishable (or anon) key, never the
   secret or service_role key.
5. Make sure .env and .env.* are in .gitignore and are not served by my host.
6. Turn off public source maps for the production build.
7. Give me a list of every key I need to rotate, with the dashboard where I rotate it.
8. Explain each change in plain English and tell me how to test that the app still works.
</code></pre>
<p>After the fix, repeat Step 1 on your live site. Your searches should come back empty, except for keys that are meant to be public.</p>
<p>Want this checked for you, plus RLS, headers, SSL and uptime? <a href="https://vibevigil.com/#scan"><strong>Run a free VibeVigil scan →</strong></a></p>
<h2>FAQ</h2>
<p><strong>Is it OK that my Supabase key is visible in my code?</strong> If it's the publishable (<code>sb_publishable_</code>) or <code>anon</code> key, yes. That's by design. Your RLS rules protect your data. See our guide on <a href="https://vibevigil.com/guides/how-to-check-if-supabase-rls-is-enabled">how to check if Supabase RLS is enabled</a>. The secret or <code>service_role</code> key must never be in the browser.</p>
<p><strong>My app is small. Would anyone really find my key?</strong> Bots scan public sites and public GitHub repos for keys all the time. It's not about how famous your app is. A key in public code should be treated as found.</p>
<p><strong>I asked my AI to "hide" the key, and it put it in a .env file. Am I safe now?</strong> Not always. If the variable name starts with <code>VITE_</code> or <code>NEXT_PUBLIC_</code>, the key still ends up in the browser. The key needs to live in server-side code.</p>
<p><strong>I deleted the key from my code. Do I still need to rotate it?</strong> Yes. Old versions of your site, GitHub history, and anyone who copied it may still have it. Make a new key and turn off the old one.</p>
<h2>Sources</h2>
<ul>
<li>Supabase docs, API keys: <a href="https://supabase.com/docs/guides/api/api-keys">https://supabase.com/docs/guides/api/api-keys</a></li>
<li>Vite docs, Env Variables and Modes: <a href="https://vite.dev/guide/env-and-mode">https://vite.dev/guide/env-and-mode</a></li>
<li>Next.js docs, Environment Variables: <a href="https://nextjs.org/docs/app/guides/environment-variables">https://nextjs.org/docs/app/guides/environment-variables</a></li>
<li>GitHub docs, Removing sensitive data from a repository: <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository">https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository</a></li>
<li>GitHub docs, About secret scanning: <a href="https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning">https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to Check if Supabase RLS Is Enabled (and Actually Working)]]></title><description><![CDATA[A plain-English checklist to see if Row Level Security is on for every Supabase table, find policies that let everyone in, and fix it with a copy-paste…]]></description><link>https://vibevigil.hashnode.dev/how-to-check-if-supabase-rls-is-enabled-and-actually-working</link><guid isPermaLink="true">https://vibevigil.hashnode.dev/how-to-check-if-supabase-rls-is-enabled-and-actually-working</guid><category><![CDATA[supabase]]></category><category><![CDATA[Security]]></category><category><![CDATA[postgres]]></category><category><![CDATA[RLS]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[VibeVigil]]></dc:creator><pubDate>Wed, 11 Nov 2026 03:36:16 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Not sure your app is safe? Run a free scan first.</strong>
VibeVigil checks your live app for Supabase tables without RLS, exposed keys, missing security headers, SSL problems and downtime. Every issue comes with a ready-to-paste prompt for your coding agent.
<strong><a href="https://vibevigil.com/#scan">Scan my app for free →</a></strong> (only scan apps you own)</p>
</blockquote>
<h1>How to Check if Supabase RLS Is Enabled (and Actually Working)</h1>
<p>If you built your app with Lovable, Bolt, v0 or Cursor, it probably talks to Supabase straight from the browser. That is normal. But it means one setting decides who can read and change your data: <strong>Row Level Security (RLS)</strong>.</p>
<p>This guide shows you how to check it in about 15 minutes. You don't need to be a developer.</p>
<h2>The short answer</h2>
<p>Open the <strong>SQL Editor</strong> in your Supabase dashboard and run:</p>
<pre><code class="language-sql">select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;
</code></pre>
<p>Any row where <code>rowsecurity</code> is <strong>false</strong> is a table with RLS turned off. Fix those first.</p>
<p>But "RLS is on" does not always mean "my data is safe". Keep reading for the other checks.</p>
<h2>Why this matters</h2>
<ul>
<li>Your Supabase <strong>anon (publishable) key</strong> sits in your frontend code. Anyone can find it. That is by design. Supabase expects RLS to protect your data, not the key being secret.</li>
<li>Supabase's docs say a table in an exposed schema (like <code>public</code>) <strong>without RLS can be read and written by any role that has access to it</strong>, and that includes the anon key. (<a href="https://supabase.com/docs/guides/database/postgres/row-level-security">Supabase: Row Level Security</a>)</li>
<li>This is not a made-up risk. In 2025, CVE-2025-48757 described Lovable-built apps that leaked data because RLS was missing or too loose. (<a href="https://nvd.nist.gov/vuln/detail/CVE-2025-48757">NVD: CVE-2025-48757</a>)</li>
</ul>
<h2>Step 1: Look for the "Unrestricted" label</h2>
<ol>
<li>Open your project in the Supabase dashboard.</li>
<li>Go to <strong>Table Editor</strong>.</li>
<li>Look at the list of tables. A table with RLS off shows an <strong>Unrestricted</strong> label. Its tooltip says the table "can be accessed by anyone via the Data API as RLS is disabled."</li>
</ol>
<p>Write down every table with that label.</p>
<h2>Step 2: Run the Security Advisor</h2>
<p>Supabase has a built-in checker.</p>
<ol>
<li>In the dashboard, open <strong>Advisors → Security Advisor</strong>.</li>
<li>Click to run it and read every item.</li>
</ol>
<p>These are the warnings that matter most for RLS (<a href="https://supabase.com/docs/guides/database/database-linter">Supabase: Database Advisors</a>):</p>
<table>
<thead>
<tr>
<th>Warning</th>
<th>What it means in plain English</th>
</tr>
</thead>
<tbody><tr>
<td><code>rls_disabled_in_public</code> ("Table publicly accessible")</td>
<td>RLS is off. Anyone with your project URL can read or change this table.</td>
</tr>
<tr>
<td><code>policy_exists_rls_disabled</code></td>
<td>You wrote rules, but RLS is off, so the rules do nothing.</td>
</tr>
<tr>
<td><code>rls_enabled_no_policy</code></td>
<td>RLS is on, but there are no rules, so the API returns nothing. Usually a "my app shows no data" bug, not a leak.</td>
</tr>
<tr>
<td><code>permissive_rls_policy</code></td>
<td>A rule like <code>USING (true)</code> lets everyone through.</td>
</tr>
<tr>
<td><code>security_definer_view</code></td>
<td>A view skips RLS and can show rows people shouldn't see.</td>
</tr>
<tr>
<td><code>rls_references_user_metadata</code></td>
<td>A rule trusts <code>user_metadata</code>, which users can edit themselves.</td>
</tr>
<tr>
<td><code>sensitive_columns_exposed</code></td>
<td>A column that looks private (like an email or token) may be public.</td>
</tr>
</tbody></table>
<p>You can also run the same checks from the terminal with the Supabase CLI: <code>supabase db advisors</code>.</p>
<h2>Step 3: Read your policies (on is not the same as safe)</h2>
<p>RLS can be on and still let everyone in. Run:</p>
<pre><code class="language-sql">select tablename, policyname, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename;
</code></pre>
<p>Look for these red flags:</p>
<ul>
<li><strong><code>qual</code> or <code>with_check</code> is just <code>true</code>.</strong> That rule lets every user through.</li>
<li><strong><code>roles</code> is <code>{public}</code> or includes <code>anon</code></strong> on a table with private data. That means logged-out visitors are allowed.</li>
<li><strong>An <code>UPDATE</code> rule with no <code>with_check</code>.</strong> A user could change a row so it belongs to someone else.</li>
<li><strong>A rule that uses <code>user_metadata</code>.</strong> Users can change that value.</li>
</ul>
<p>A good rule for private data usually looks like "only the owner can see this row":</p>
<pre><code class="language-sql">create policy "Users can read their own notes"
on public.notes for select
to authenticated
using ( (select auth.uid()) = user_id );
</code></pre>
<h2>Step 4: Check who has access at all (grants)</h2>
<p>Supabase has two layers: <strong>grants</strong> (can this role touch the table at all?) and <strong>policies</strong> (which rows?). Adding policies does <strong>not</strong> remove the default grants. Run:</p>
<pre><code class="language-sql">select table_name, grantee, privilege_type
from information_schema.role_table_grants
where table_schema = 'public'
  and grantee in ('anon', 'authenticated')
order by table_name, grantee;
</code></pre>
<p>If <code>anon</code> has <code>DELETE</code>, <code>UPDATE</code> or <code>INSERT</code> on a table that logged-out visitors should never change, remove it (see the fix below).</p>
<p><strong>As of October 30, 2026:</strong> Supabase now applies this to existing projects: new tables in <code>public</code> no longer get these grants automatically. You need to add a <code>grant</code> in the same migration where you turn on RLS. Tables you already had keep their current grants, so you still need to check them. (<a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically">Supabase changelog</a>)</p>
<h2>Step 5: Check views and database functions</h2>
<p>Two things can skip RLS even when every table is protected:</p>
<p><strong>Views.</strong> By default, a Postgres view runs with the rights of the person who created it, so it ignores RLS. Find your views:</p>
<pre><code class="language-sql">select c.relname as view_name, c.reloptions
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v';
</code></pre>
<p>If <code>reloptions</code> does not include <code>security_invoker=true</code> (or <code>on</code>), the view skips RLS. On Postgres 15 and newer you can fix it with:</p>
<pre><code class="language-sql">alter view public.my_view set (security_invoker = true);
</code></pre>
<p><strong>Functions marked <code>security definer</code>.</strong> Functions in the <code>public</code> schema can be called through your API at <code>/rest/v1/rpc/...</code>. If one is <code>security definer</code>, it runs with full rights. Make sure logged-out visitors can't call it:</p>
<pre><code class="language-sql">revoke execute on function public.my_function from public, anon;
</code></pre>
<h2>Step 6: Test your own app like a stranger</h2>
<p>Do this only on an app you own.</p>
<ol>
<li>Open your app in a private (incognito) window. Don't log in.</li>
<li>Visit every page. Does any private data show up?</li>
<li>Create two test accounts. Log in as account A and add some data. Log in as account B. Can B see or change A's data?</li>
</ol>
<p>If anything leaks, go back to Steps 3–5.</p>
<h2>If you use Lovable</h2>
<p>Lovable has a <strong>Security</strong> view. A <strong>quick scan</strong> runs on every publish and checks the database for tables without RLS and rules that let everyone through. You can also run a <strong>deep scan</strong> on demand. Lovable's own docs say these scans don't guarantee your app is fully secure, so still do Steps 3–6. (<a href="https://docs.lovable.dev/features/security">Lovable docs: Security</a>)</p>
<h2>How to fix it</h2>
<h3>Option A: fix it yourself in SQL</h3>
<p>Replace <code>notes</code> and <code>user_id</code> with your table and owner column:</p>
<pre><code class="language-sql">-- 1. Turn RLS on
alter table public.notes enable row level security;

-- 2. Remove access you don't need, then give back only what's needed
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on table public.notes to authenticated;

-- 3. One rule per action: users only touch their own rows
create policy "notes_select_own" on public.notes for select to authenticated
  using ( (select auth.uid()) = user_id );
create policy "notes_insert_own" on public.notes for insert to authenticated
  with check ( (select auth.uid()) = user_id );
create policy "notes_update_own" on public.notes for update to authenticated
  using ( (select auth.uid()) = user_id )
  with check ( (select auth.uid()) = user_id );
create policy "notes_delete_own" on public.notes for delete to authenticated
  using ( (select auth.uid()) = user_id );
</code></pre>
<p>If some data really is public (like a blog post list), give <code>anon</code> only <code>select</code> on that table, and write a <code>select</code> rule that shows only the public rows.</p>
<h3>Option B: paste this prompt into your coding agent</h3>
<p>Works in Lovable, Bolt, v0, Cursor or Claude Code:</p>
<pre><code class="language-text">Audit my Supabase database security and fix it. Do not delete any data.

1. List every table in the public schema and say whether Row Level Security is enabled.
2. Enable RLS on every table that doesn't have it.
3. For each table, tell me who should be able to read, insert, update and delete rows
   (logged-out visitors, any logged-in user, or only the row's owner). Ask me if unsure.
4. Revoke the default grants from anon and authenticated, then grant back only what each table needs.
5. Write one policy per operation using "to authenticated" and "(select auth.uid()) = user_id".
   Every UPDATE policy must have both USING and WITH CHECK. Never use USING (true) on private data.
   Never use user_metadata in a policy.
6. Make every view in public use security_invoker = true.
7. Revoke EXECUTE from anon and public on any SECURITY DEFINER function that visitors shouldn't call.
8. Confirm the service_role / secret key is not used anywhere in frontend code or VITE_/NEXT_PUBLIC_ variables.
9. Put all changes in one migration file and explain each change in plain English.
10. Tell me how to test it: logged out, and with two different user accounts.
</code></pre>
<p>After the fix, run the Security Advisor again (Step 2) and repeat the stranger test (Step 6).</p>
<p>Want this checked for you, plus exposed keys, headers, SSL and uptime? <strong><a href="https://vibevigil.com/#scan">Run a free VibeVigil scan →</a></strong></p>
<h2>FAQ</h2>
<p><strong>Is it OK that my Supabase anon key is visible in my code?</strong>
Yes. The anon (publishable) key is meant to be public. RLS is what protects your data. The <strong>service_role</strong> (secret) key is different: it skips RLS completely and must never be in your frontend.</p>
<p><strong>I turned on RLS and now my app shows no data. Why?</strong>
RLS with no policies blocks everything. That's safe, but your app needs rules. Add <code>select</code> rules for the data each user should see (Step 3).</p>
<p><strong>My AI fixed an RLS error by writing <code>USING (true)</code>. Is that OK?</strong>
Usually not. That rule lets everyone through, which is the same as having no protection for that action. Replace it with an owner check like <code>(select auth.uid()) = user_id</code>.</p>
<p><strong>What changed on October 30, 2026?</strong>
For existing Supabase projects, new tables in <code>public</code> are no longer reachable from the API until you add a <code>grant</code>. Old tables keep their access, so check them with Step 4.</p>
<p><strong>Lovable says my app passed its security scan. Am I done?</strong>
It's a good start. Lovable itself says its scans don't guarantee full security. Do the policy, grants, view and stranger checks above too.</p>
<h2>Sources</h2>
<ul>
<li>Supabase docs, Row Level Security: <a href="https://supabase.com/docs/guides/database/postgres/row-level-security">https://supabase.com/docs/guides/database/postgres/row-level-security</a></li>
<li>Supabase docs, Database Advisors (lints): <a href="https://supabase.com/docs/guides/database/database-linter">https://supabase.com/docs/guides/database/database-linter</a></li>
<li>Supabase changelog, breaking change for table grants (Oct 30, 2026): <a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically">https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically</a></li>
<li>Lovable docs, Security: <a href="https://docs.lovable.dev/features/security">https://docs.lovable.dev/features/security</a></li>
<li>NVD, CVE-2025-48757: <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-48757">https://nvd.nist.gov/vuln/detail/CVE-2025-48757</a></li>
</ul>
]]></content:encoded></item></channel></rss>