<?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[BuiltSafely]]></title><description><![CDATA[BuiltSafely]]></description><link>https://builtsafely.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 29 Oct 2026 09:19:16 GMT</lastBuildDate><atom:link href="https://builtsafely.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Securing a Lovable app before you publish: scans, Secrets and RLS]]></title><description><![CDATA[How to use Lovable's built-in security scans, keep API keys in Secrets, review RLS policies and sign-in methods before real users arrive.]]></description><link>https://builtsafely.hashnode.dev/securing-a-lovable-app-before-you-publish</link><guid isPermaLink="true">https://builtsafely.hashnode.dev/securing-a-lovable-app-before-you-publish</guid><category><![CDATA[Security]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[AI]]></category><category><![CDATA[supabase]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[BuiltSafely]]></dc:creator><pubDate>Wed, 28 Oct 2026 14:13:06 GMT</pubDate><content:encoded><![CDATA[<p>Shipping a Lovable app is fast, and the security tooling that comes with it is genuinely useful, but it only helps if you run it and read what it finds. This walkthrough covers the six things worth doing before real users sign up: both scans, secrets, database rules, leaked-password protection, sign-in methods and a focused review. Every step points to the Lovable doc it comes from, so you can check the current wording yourself.</p>
<p><em>This guide first appeared on BuiltSafely: <a href="https://builtsafely.com/guides/lovable/">https://builtsafely.com/guides/lovable/</a></em></p>
<p><strong>Label for every step in this guide: Docs-only.</strong> We wrote it from Lovable's public docs (last reviewed October 21, 2026) and did not run these steps end to end. Source overview: <a href="https://docs.lovable.dev/features/security">Lovable docs: Security overview</a>.</p>
<p>Lovable's own docs say its tools "do not replace a thorough security review" and that you are responsible for your app's security. Here's how to use them well.</p>
<h2>Copy this fix</h2>
<p>Run both security scans first, then paste this prompt into the project chat. Treat the answer as a to-do list to check, not a guarantee.</p>
<pre><code class="language-text">Review the authentication flow and every server-side function in my app. For each one,
tell me who can call it, what data it can read or change, and whether it checks the
caller's identity and role.
</code></pre>
<h2>1. Run both scans</h2>
<p><em>Docs-only</em></p>
<ul>
<li><strong>Quick scan</strong> runs automatically when you open the publish dialog. It checks database access rules (for example, tables without row-level security, or rules that let everyone through), dependency vulnerabilities, and MCP servers exposed without authentication.</li>
<li><strong>Deep scan</strong> doesn't run automatically. Run it from the project's Security view before you publish. It reviews your app code for access-control gaps, unauthenticated endpoints, injection, leaked secrets, payment/billing logic and auth weaknesses.</li>
<li>Fix every <strong>Critical</strong> finding. Read each <strong>Warning</strong> and decide on purpose. Don't just ignore it.</li>
</ul>
<p>A scan is a starting point, not a sign-off.</p>
<h2>2. Keep API keys in Secrets, and call APIs from server-side code</h2>
<p><em>Docs-only</em></p>
<p>Lovable's frontend runs in the browser, so secrets can't be stored safely there. Its docs recommend storing keys in <strong>Secrets</strong> and making third-party API calls from server-side functions, which your frontend then calls.</p>
<ul>
<li>Manage secrets in <strong>More → Settings → Secrets</strong> (<a href="https://docs.lovable.dev/features/secrets">Lovable docs: Secrets</a>).</li>
<li>Your server-side functions are listed in <strong>More → Database → Manage → Edge functions</strong> (<a href="https://docs.lovable.dev/features/edge-functions">Lovable docs: Edge functions</a>).</li>
<li>If you pasted a key into chat, move it to Secrets and rotate it at the provider.</li>
</ul>
<p>More on this: <a href="https://builtsafely.com/fixes/secret-key-in-frontend-bundle/">Check what your build actually ships: secret keys in frontend env vars</a>.</p>
<h2>3. Review your RLS policies yourself</h2>
<p><em>Docs-only</em></p>
<p>Lovable's docs call misconfigured RLS "a common cause of data leaks". To review every policy, go to <strong>More → Database</strong>, select <strong>Manage</strong>, then <strong>Policies</strong>. Each row shows the policy name, the command it covers (<code>SELECT</code>, <code>INSERT</code>, <code>UPDATE</code> or <code>DELETE</code>), who it applies to (such as signed-in <code>authenticated</code> or <code>anon</code> users), and whether it has a condition. Expand a row to read its SQL condition.</p>
<p>The view is read-only, so to change a policy, ask Lovable in the project chat, then check the result in the view (<a href="https://docs.lovable.dev/features/database-access">Lovable docs: Control who can access your app's data</a>). Then read our <a href="https://builtsafely.com/guides/supabase-rls/">Supabase RLS guide</a> and test with two accounts.</p>
<p>On the built-in backend you can also ask Lovable to <strong>rotate the database password</strong>, for example:</p>
<pre><code class="language-text">Rotate my database password.
</code></pre>
<p>Lovable asks you to confirm, doesn't show the new password, and your app keeps working. Any BI tool or script that someone connected with the old password stops working until it's updated. This applies only to the built-in backend, not to a connected Supabase project (same Lovable page).</p>
<p><strong>Check:</strong> in <strong>More → Database → Manage → Policies</strong>, each policy's <strong>Applies to</strong> and condition match who should see or change those rows.</p>
<h2>4. Turn on leaked-password protection</h2>
<p><em>Docs-only</em></p>
<p>If your app uses email and password sign-in, turn on <strong>Password HIBP check</strong> in <strong>More → Users → Settings → Email</strong>. It rejects passwords found in the Have I Been Pwned database of leaked passwords (<a href="https://docs.lovable.dev/features/email-auth">Lovable docs: Email authentication</a>). If you connected Supabase directly, use the Supabase equivalent.</p>
<h2>5. Review your sign-in methods</h2>
<p><em>Docs-only</em></p>
<p>Unless you name the methods you want, Lovable's login pages for apps on the built-in backend (Cloud) include email and password, Google, and <strong>Sign in with Lovable</strong> (<a href="https://docs.lovable.dev/changelog">Lovable changelog, Oct 15, 2026</a>). With Sign in with Lovable, "Any Lovable account can sign in," and your app receives the person's name, email address and profile picture (<a href="https://docs.lovable.dev/features/lovable-auth">Lovable docs: Sign in with Lovable</a>).</p>
<ol>
<li>Open <strong>More → Users → Settings</strong>. The <strong>Sign in methods</strong> list shows each method as <strong>Enabled</strong> or <strong>Disabled</strong>.</li>
<li>Turn off any method you don't mean to offer.</li>
<li>Use <strong>Disable sign-up</strong> if new people shouldn't be able to join (<a href="https://docs.lovable.dev/features/authentication">Lovable docs: Users and authentication</a>).</li>
<li>Keep at least one method enabled so existing users can get back in.</li>
</ol>
<p><strong>OpenID Connect (OIDC) sign-in</strong> lets your users sign in with your own identity provider, such as Auth0, Okta, Microsoft Entra ID or Keycloak. Your provider then decides who can sign in and applies its own password and MFA rules (<a href="https://docs.lovable.dev/features/oidc-auth">Lovable docs: OIDC sign-in</a>). Steps and limits, all from that page:</p>
<ul>
<li><strong>What it needs:</strong> a project on the built-in backend (Cloud) with authentication on, edit access, and admin access to your OIDC provider. It's available on all plans. On Business and Enterprise workspaces, <strong>SAML SSO</strong> must be allowed under <strong>App login methods</strong>, because that one setting covers OIDC too. It isn't available for self-hosted backends.</li>
<li><strong>Set it up:</strong> go to <strong>More → Users → Settings → OIDC</strong>, enable <strong>OIDC sign-in</strong> and copy the <strong>Redirect URI</strong>. Register a web application with your provider and add that redirect URI. Then enter the <strong>Issuer URL</strong> (must start with <code>https://</code>), <strong>Client ID</strong> and <strong>Client secret</strong> in the form and save. Lovable runs a build that adds the sign-in button, and that build uses credits.</li>
<li><strong>Keep the client secret out of chat:</strong> enter it in the OIDC form, not in the project chat. Lovable keeps it masked. To rotate it, enter the new secret and save.</li>
<li><strong>Test it:</strong> open your sign-in page in a private window, use the new button, and confirm you come back signed in.</li>
<li><strong>Know the limits:</strong> one OIDC provider per project, and the scopes are fixed to <code>openid</code>, <code>profile</code> and <code>email</code>.</li>
</ul>
<p>If an app or website you didn't build on Lovable signs people in through Lovable, it uses an OAuth client that a workspace admin registers under <strong>Workspace settings → API keys &amp; OAuth → OAuth clients</strong>. A Web server client's secret is shown only once. Lovable says: "Do not put it in browser code, a public repository, or a prompt." Scopes that read or change projects and workspaces need a Business or Enterprise plan and verification by Lovable (<a href="https://docs.lovable.dev/features/oauth-clients">Lovable docs: OAuth clients</a>).</p>
<p><strong>Check:</strong> your login page offers only the sign-in methods you chose, and every other method shows <strong>Disabled</strong> in <strong>More → Users → Settings</strong>.</p>
<h2>6. Ask for a focused review, then verify</h2>
<p><em>Docs-only</em></p>
<p>Use the prompt from <strong>Copy this fix</strong> above. Conversational reviews use credits (per Lovable's docs). Treat the answer as a to-do list to check, not a guarantee.</p>
<p>For apps handling sensitive data or payments, Lovable's docs suggest an additional professional security review. We agree.</p>
<h2>Related</h2>
<ul>
<li><a href="https://builtsafely.com/fixes/secret-key-in-frontend-bundle/">Check what your build actually ships: secret keys in frontend env vars</a></li>
<li><a href="https://builtsafely.com/fixes/new-row-violates-rls/">new row violates row-level security policy (Supabase)</a></li>
<li><a href="https://builtsafely.com/guides/supabase-rls/">Supabase row-level security, explained for builders</a></li>
</ul>
<p><em>This guide is educational and based on the vendor's public docs as of the review date. Platforms change: check the linked sources. Not a security audit. <a href="https://builtsafely.com/disclaimer/">Disclaimer</a>.</em></p>
<hr />
<p>More fixes like this at <a href="https://builtsafely.com/fixes/">https://builtsafely.com/fixes/</a></p>
]]></content:encoded></item><item><title><![CDATA[infinite recursion detected in policy for relation (42P17)]]></title><description><![CDATA[What the Postgres 42P17 RLS recursion error means in Supabase, and the safe fix with a private SECURITY DEFINER helper, without turning RLS off.]]></description><link>https://builtsafely.hashnode.dev/infinite-recursion-detected-in-policy-for-relation-42p17</link><guid isPermaLink="true">https://builtsafely.hashnode.dev/infinite-recursion-detected-in-policy-for-relation-42p17</guid><category><![CDATA[supabase]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Security]]></category><category><![CDATA[SQL]]></category><category><![CDATA[database]]></category><dc:creator><![CDATA[BuiltSafely]]></dc:creator><pubDate>Tue, 27 Oct 2026 14:20:50 GMT</pubDate><content:encoded><![CDATA[<p>If your Supabase queries suddenly fail with "infinite recursion detected in policy for relation", the cause is almost always a row-level security policy that ends up checking itself. The tempting shortcut is to switch RLS off, which removes the error by removing your access rules. Below is the fix we tested instead: move the lookup into a small private helper function and keep RLS on.</p>
<p><em>This guide first appeared on BuiltSafely: <a href="https://builtsafely.com/fixes/infinite-recursion-policy/">https://builtsafely.com/fixes/infinite-recursion-policy/</a></em></p>
<p><strong>Last checked October 19, 2026 · Educational, not an audit.</strong></p>
<h2>The fix</h2>
<p><strong>Verified:</strong> we ran this in a throwaway local Postgres 17 database (not a full Supabase stack). Details under <em>What we tested</em> below.</p>
<p>Stop the policy from querying its own table (or a chain of tables that loops back). Move the lookup into a <code>SECURITY DEFINER</code> function in a private schema, pin <code>search_path</code>, then call that function from the policy. Keep RLS on.</p>
<pre><code class="language-sql">create schema if not exists private;

-- Look up the caller's team ids without re-entering the policy
create or replace function private.my_team_ids()
returns setof uuid
language sql
stable
security definer
set search_path = ''
as $$
  select team_id
  from public.members
  where user_id = (select auth.uid())
$$;

revoke all on function private.my_team_ids() from public;
grant usage on schema private to authenticated;
grant execute on function private.my_team_ids() to authenticated;

drop policy if exists "members read their teams" on public.members;
create policy "members read their teams"
on public.members
for select
to authenticated
using ( team_id in (select private.my_team_ids()) );
</code></pre>
<p>Replace <code>members</code> and <code>team_id</code> with your table and columns.</p>
<p>That shape follows Supabase's security-definer guidance: run as the creator (which bypasses RLS on the lookup), pin <code>search_path = ''</code>, schema-qualify names, and keep the function out of an exposed schema (<a href="https://supabase.com/docs/guides/database/postgres/row-level-security">Supabase: RLS</a>).</p>
<p><strong>What we tested (October 19, 2026):</strong> on a throwaway local Postgres 17, a self-referencing <code>SELECT</code> policy on <code>members</code> raised <code>ERROR: infinite recursion detected in policy for relation "members"</code>. After adding the private helper above (with a stand-in JWT claim for <code>auth.uid()</code>), a user saw only their team's rows, and a second user saw both teams they belonged to.</p>
<h2>Why it happens</h2>
<p><em>Docs-only</em></p>
<p>Supabase's troubleshooting page says this error is "infinite recursion in your Row-Level Security (RLS) policies." It happens when a policy queries a table that has RLS, and that table's policies end up triggering the original policy again: either the same table, or another table that loops back (<a href="https://supabase.com/docs/guides/troubleshooting/storage-error-database-schema-is-incompatible-when-uploading-files-988baa">Supabase troubleshooting: RLS policy causes infinite recursion</a>). Postgres raises SQLSTATE <code>42P17</code> and aborts the query (<a href="https://www.postgresql.org/docs/current/errcodes-appendix.html">PostgreSQL error codes</a>).</p>
<p>A common pattern is "show every member of any team I belong to" written as a subquery on the same <code>members</code> table. Evaluating the policy runs the subquery, which re-enters the policy.</p>
<h2>What not to do</h2>
<p><em>Docs-only</em></p>
<ul>
<li>Don't disable RLS to clear the error.</li>
<li>Don't put a secret or <code>service_role</code> key in the client to "bypass" it (<a href="https://supabase.com/docs/guides/getting-started/api-keys">Supabase: API keys</a>).</li>
<li>Don't leave a <code>SECURITY DEFINER</code> function in an exposed schema, and don't skip <code>set search_path = ''</code>. Supabase warns both are risky (<a href="https://supabase.com/docs/guides/database/postgres/row-level-security">Supabase: RLS</a>).</li>
</ul>
<h2>Copy this fix: AI prompt</h2>
<p>Paste this into your AI coding tool:</p>
<pre><code class="language-text">My app hits: infinite recursion detected in policy for relation (42P17).
Find any RLS policy that queries its own table or forms a policy loop.
Propose a private SECURITY DEFINER helper with set search_path = '', revoke public execute, and a rewritten policy that calls it.
Do not disable RLS. Do not use a secret or service_role key in client code.
Show the SQL first and wait for my OK before applying anything.
</code></pre>
<h2>How to verify (two accounts)</h2>
<p><strong>Verified</strong> in our local test with two stand-in users; run it against your own app too.</p>
<ol>
<li>Sign in as user A. Confirm you can read the rows you should (for example your team's members).</li>
<li>Sign in as user B on another team. Confirm you see B's teams, not A's private rows.</li>
<li>Sign out. Confirm signed-out requests get no private rows.</li>
</ol>
<h2>Related</h2>
<ul>
<li><a href="https://builtsafely.com/guides/supabase-rls/">Our Supabase RLS guide</a></li>
<li><a href="https://builtsafely.com/fixes/new-row-violates-rls/">new row violates row-level security policy</a></li>
<li><a href="https://builtsafely.com/fixes/supabase-empty-array-rls/">Empty array with RLS on</a></li>
</ul>
<p><em>Educational, not a security audit. Platforms change: check the linked sources. <a href="https://builtsafely.com/disclaimer/">Disclaimer</a>.</em></p>
<hr />
<p>More fixes like this at <a href="https://builtsafely.com/fixes/">https://builtsafely.com/fixes/</a></p>
]]></content:encoded></item><item><title><![CDATA[42501 permission denied for table]]></title><description><![CDATA[What the Supabase 42501 "permission denied for table" error means, and the safe fix: add a GRANT and keep RLS on.]]></description><link>https://builtsafely.hashnode.dev/42501-permission-denied-for-table</link><guid isPermaLink="true">https://builtsafely.hashnode.dev/42501-permission-denied-for-table</guid><category><![CDATA[supabase]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Security]]></category><category><![CDATA[database]]></category><category><![CDATA[SQL]]></category><dc:creator><![CDATA[BuiltSafely]]></dc:creator><pubDate>Mon, 26 Oct 2026 14:24:42 GMT</pubDate><content:encoded><![CDATA[<p>A 42501 "permission denied for table" error means the database role making the request was never granted access to that table, so Postgres stops before your row-level security policies even run. It often shows up after adding a table or running a migration, and some clients report it as a plain 401 or 403. The fix is a precise grant, not turning security off; here is how to find the missing privilege and add it safely.</p>
<p><em>This guide first appeared on BuiltSafely: <a href="https://builtsafely.com/fixes/42501-permission-denied/">https://builtsafely.com/fixes/42501-permission-denied/</a></em></p>
<p><strong>Last checked October 19, 2026 · Educational, not an audit.</strong></p>
<h2>The fix</h2>
<p><strong>Verified:</strong> we ran this against a throwaway local Supabase Postgres 17 container (no real project). Details under <em>What we tested</em> below.</p>
<p>Grant the role the privilege it's missing, in a migration, and keep RLS on. The error's hint tells you which role and privilege it needs:</p>
<pre><code class="language-sql">-- Signed-in users manage their own notes; signed-out visitors get nothing
grant select, insert, update, delete on table public.notes to authenticated;
alter table public.notes enable row level security;
-- ...plus one policy per operation (see our RLS guide)
</code></pre>
<p>Replace <code>notes</code> with your table. Grant only what each role needs. Only grant to <code>anon</code> for data that's meant to be public.</p>
<p><strong>What we tested (October 16, 2026):</strong> without a grant, a signed-out read failed with <code>42501: permission denied for table notes</code> and the hint "Grant the required privileges to the current role with: GRANT SELECT ON public.notes TO anon;". A public table with <code>grant select ... to anon</code>, RLS on and a read-only policy returned its rows to <code>anon</code>.</p>
<h2>Why it happens</h2>
<p><em>Docs-only, except where marked</em></p>
<p>Postgres checks grants before policies. Supabase's RLS guide says a missing grant "raises a <code>42501</code> error before any policy runs", so check grants before you change policies (<a href="https://supabase.com/docs/guides/database/postgres/row-level-security">Supabase: RLS</a>).</p>
<p>To see a table's grants (<a href="https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors">Supabase: 42501 errors</a>):</p>
<pre><code class="language-sql">select grantee, privilege_type
from information_schema.role_table_grants
where table_name = 'notes';
</code></pre>
<p><strong>Verified:</strong> on the same test container, this listed the roles and privileges for the table.</p>
<p><strong>Why you may see it more on tables created on or after October 30, 2026:</strong> Supabase applies a changed default to all existing projects on that date. <strong>Existing tables keep their current grants and stay reachable.</strong> Only tables created in <code>public</code> after the change need an explicit grant before the Data API can see them. Direct Postgres connections are unaffected (<a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically">Supabase changelog</a>). Our <a href="https://builtsafely.com/guides/supabase-oct30-grants/">Oct 30 grants guide</a> covers the migration pattern.</p>
<p>Clients often surface 42501 as a 401 or 403, so read the response body for <code>permission denied for table</code> (<a href="https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors">Supabase: 42501 errors</a>). Not every 42501 is a missing grant: Supabase also lists restricted schemas, custom schemas without API grants, column-level restrictions, and RLS blocking an insert or update. If the message says "new row violates row-level security policy", see our <a href="https://builtsafely.com/fixes/new-row-violates-rls/">RLS error fix</a>.</p>
<p>If you limited updates to certain columns on purpose, for example so users can't change their own role, a 42501 on those columns is that protection working. See <a href="https://builtsafely.com/guides/editable-access-field/">Never let users edit the field your access rule trusts</a>.</p>
<h2>What not to do</h2>
<p><em>Docs-only</em></p>
<ul>
<li>Don't disable RLS.</li>
<li>Don't grant everything to <code>anon</code> without policies.</li>
<li>Don't move the secret or <code>service_role</code> key into the frontend (<a href="https://supabase.com/docs/guides/getting-started/api-keys">Supabase: API keys</a>).</li>
</ul>
<p>Treat grant + enable RLS + policies as one unit in the same migration (<a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically">Supabase changelog</a>).</p>
<h2>Copy this fix</h2>
<pre><code class="language-sql">-- Replace notes with your table. Signed-out visitors get nothing.
grant select, insert, update, delete on table public.notes to authenticated;
alter table public.notes enable row level security;
-- ...plus one policy per operation (see our RLS guide)
</code></pre>
<p>Grant only what each role needs, and keep RLS on. Policy examples: <a href="https://builtsafely.com/guides/supabase-rls/">Supabase row-level security, explained for builders</a>.</p>
<h2>Related</h2>
<ul>
<li><a href="https://builtsafely.com/fixes/new-row-violates-rls/">new row violates row-level security policy</a></li>
<li><a href="https://builtsafely.com/guides/supabase-oct30-grants/">Supabase's Oct 30 change: new tables need a GRANT</a></li>
<li><a href="https://builtsafely.com/fixes/infinite-recursion-policy/">infinite recursion detected in policy for relation (42P17)</a></li>
</ul>
<p><em>Educational, not a security audit. Platforms change: check the linked sources. <a href="https://builtsafely.com/disclaimer/">Disclaimer</a>.</em></p>
<hr />
<p>More fixes like this at <a href="https://builtsafely.com/fixes/">https://builtsafely.com/fixes/</a></p>
]]></content:encoded></item></channel></rss>