<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: FixLoop</title>
    <description>The latest articles on DEV Community by FixLoop (@fixloopdev).</description>
    <link>https://dev.to/fixloopdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4190902%2F4defce50-e3af-4663-bdab-b19a86eeed7c.png</url>
      <title>DEV Community: FixLoop</title>
      <link>https://dev.to/fixloopdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fixloopdev"/>
    <language>en</language>
    <item>
      <title>Lovable custom domain stuck on Verifying? It is almost always one of five DNS or auth settings</title>
      <dc:creator>FixLoop</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:02:12 +0000</pubDate>
      <link>https://dev.to/fixloopdev/lovable-custom-domain-stuck-on-verifying-it-is-almost-always-one-of-five-dns-or-auth-settings-20jf</link>
      <guid>https://dev.to/fixloopdev/lovable-custom-domain-stuck-on-verifying-it-is-almost-always-one-of-five-dns-or-auth-settings-20jf</guid>
      <description>&lt;p&gt;Your Lovable custom domain is not working: it sits on &lt;strong&gt;Verifying&lt;/strong&gt;, the browser shows a security warning, it loads your old website, or the site works but login fails. From the guide: "Almost every case is one of five DNS or auth settings, and Lovable's domain status tells you which one."&lt;/p&gt;

&lt;p&gt;Open Project settings, Domains, read the status next to the domain, then fix the matching cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The records Lovable expects
&lt;/h2&gt;

&lt;p&gt;For a domain bought elsewhere and connected with manual setup, Lovable gives you two records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A&lt;/strong&gt; record on &lt;code&gt;@&lt;/code&gt;: &lt;code&gt;185.158.133.1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TXT&lt;/strong&gt; record on &lt;code&gt;_lovable&lt;/code&gt;: &lt;code&gt;lovable_verify=...&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then check that nothing else is fighting them: no AAAA record on that host, no leftover A or CNAME from your old host, and no CAA record that blocks Lovable's certificate authorities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuck on "Verifying" or "Unable to verify"
&lt;/h2&gt;

&lt;p&gt;Lovable cannot see the records it asked for yet. In order of how often it shows up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The A record is proxied.&lt;/strong&gt; In Cloudflare, an orange-cloud A record hides Lovable's IP. From the guide: "Lovable's docs say that while verification is in progress you should not enable proxying on the A record or point it at a CDN. Switch it to DNS only (grey cloud)."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The TXT value is cut off or on the wrong host.&lt;/strong&gt; The host is &lt;code&gt;_lovable&lt;/code&gt; and the whole &lt;code&gt;lovable_verify=&lt;/code&gt; value has to be there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A couple of other notes from the guide: custom domains need a paid Lovable plan, and the project has to be published before the domain serves anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  If login breaks on the new domain
&lt;/h2&gt;

&lt;p&gt;Adding a domain does not update Supabase Auth. Your new URL has to be in the Supabase redirect URL list too, or sign-in loops back to the old host.&lt;/p&gt;

&lt;p&gt;Full guide with all five causes and the record table: &lt;a href="https://fixloop.dev/guides/lovable-custom-domain-not-working" rel="noopener noreferrer"&gt;fixloop.dev/guides/lovable-custom-domain-not-working&lt;/a&gt;. Stuck on it right now? Paste the error at &lt;a href="https://fixloop.dev/start?type=diagnose&amp;amp;src=devto" rel="noopener noreferrer"&gt;fixloop.dev/start&lt;/a&gt; and we will email the likely cause within 24 hours, free. (I run FixLoop.)&lt;/p&gt;

</description>
      <category>lovable</category>
      <category>dns</category>
      <category>supabase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Free tool: find Supabase tables that will 42501 after Oct 30</title>
      <dc:creator>FixLoop</dc:creator>
      <pubDate>Fri, 23 Oct 2026 13:11:35 +0000</pubDate>
      <link>https://dev.to/fixloopdev/free-tool-find-supabase-tables-that-will-42501-after-oct-30-3jd3</link>
      <guid>https://dev.to/fixloopdev/free-tool-find-supabase-tables-that-will-42501-after-oct-30-3jd3</guid>
      <description>&lt;p&gt;On &lt;strong&gt;October 30, 2026&lt;/strong&gt;, existing Supabase projects stop auto-granting privileges on new tables in the &lt;code&gt;public&lt;/code&gt; schema. Source: &lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;Supabase changelog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What that means in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Existing tables are not affected.&lt;/strong&gt; They keep their current grants and stay reachable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New tables in &lt;code&gt;public&lt;/code&gt;&lt;/strong&gt; created after the change without a GRANT return Postgres error &lt;code&gt;42501&lt;/code&gt;, &lt;code&gt;permission denied for table&lt;/code&gt;, through the Data API (supabase-js, REST, GraphQL). RLS policies don't help, because Postgres checks grants before RLS.&lt;/li&gt;
&lt;li&gt;Direct Postgres connections, and the &lt;code&gt;auth&lt;/code&gt; and &lt;code&gt;storage&lt;/code&gt; schemas, are not part of this change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix, if you just want the SQL
&lt;/h2&gt;

&lt;p&gt;Put the grants in the same migration as the table and its policies, least privilege:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- only if signed-out visitors should read it:&lt;/span&gt;
&lt;span class="c1"&gt;-- grant select on table public.your_table to anon;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip &lt;code&gt;grant all on all tables in schema public&lt;/code&gt;. That also opens tables where RLS was never turned on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checker
&lt;/h2&gt;

&lt;p&gt;If you have a pile of migrations (or Lovable keeps writing new ones), I built a free page that does the tedious part:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fixloop.dev/tools/supabase-grants-checker?src=devto" rel="noopener noreferrer"&gt;Supabase grants checker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Paste one or more migration files.&lt;/li&gt;
&lt;li&gt;It lists new &lt;code&gt;public&lt;/code&gt; tables that are missing a GRANT for &lt;code&gt;authenticated&lt;/code&gt; or &lt;code&gt;service_role&lt;/code&gt; (it also subtracts any later REVOKE in the paste).&lt;/li&gt;
&lt;li&gt;Copy the GRANT statements it writes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It runs entirely in your browser. The SQL is never sent anywhere, and there's no signup. Comments and string literals are ignored, and tables in &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;storage&lt;/code&gt; or other schemas are skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell your AI builder
&lt;/h2&gt;

&lt;p&gt;Lovable and other AI builders learned the old default: create table, enable RLS, add policies, done. Add this to your project instructions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you create a Supabase table, put CREATE TABLE, ENABLE ROW LEVEL SECURITY, the policies and explicit GRANTs to authenticated and service_role in the same migration. Grant to anon only if signed-out users should read it. Never use GRANT ALL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Full walkthrough: &lt;a href="https://fixloop.dev/guides/supabase-oct-30-grants-change?src=devto" rel="noopener noreferrer"&gt;Supabase Oct 30 grants change: fix 42501 on new tables&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Still stuck on a 42501 (or anything else in a Lovable/Supabase app)? &lt;a href="https://fixloop.dev/start?type=diagnose&amp;amp;src=devto" rel="noopener noreferrer"&gt;Paste the error and get a free diagnosis within 24 hours&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>lovable</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Supabase "Email address not authorized": why users never get your signup email</title>
      <dc:creator>FixLoop</dc:creator>
      <pubDate>Thu, 22 Oct 2026 13:06:47 +0000</pubDate>
      <link>https://dev.to/fixloopdev/supabase-email-address-not-authorized-why-users-never-get-your-signup-email-2jdg</link>
      <guid>https://dev.to/fixloopdev/supabase-email-address-not-authorized-why-users-never-get-your-signup-email-2jdg</guid>
      <description>&lt;p&gt;Sign-up works when you test it. Then real users sign up and the confirmation email never shows up, or &lt;code&gt;supabase.auth.signUp()&lt;/code&gt; comes back with &lt;code&gt;Email address not authorized&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nothing is broken. Your project is still on Supabase's built-in email service, and that service only mails your own team. From the docs: "Unless you configure a custom SMTP server for your project, Supabase Auth will refuse to deliver messages to addresses that are not part of the project's team."&lt;/p&gt;

&lt;p&gt;You're on the team, so your test emails arrive. Your users aren't, so theirs don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits of the default service
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Team addresses only.&lt;/strong&gt; "All other addresses will fail with the error message Email address not authorized."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tiny rate limit.&lt;/strong&gt; "Currently this value is set to 2 messages per hour," and Supabase says it can change without notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best effort.&lt;/strong&gt; It's "not meant for production use."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It applies to every Auth email: confirmations, magic links, one-time codes, invites, email change and password reset.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: bring your own SMTP
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick a provider.&lt;/strong&gt; Supabase lists Resend, AWS SES, Postmark, Twilio SendGrid, ZeptoMail and Brevo as examples. Anything that speaks SMTP works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify a sending domain&lt;/strong&gt; with SPF, DKIM and DMARC. Use something like &lt;code&gt;no-reply@auth.yourdomain.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable custom SMTP&lt;/strong&gt; in your project's Authentication settings: host, port, user, password, sender email, sender name. Save.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise the rate limit.&lt;/strong&gt; Custom SMTP starts at "a low rate-limit of 30 messages per hour." Bump it on the Auth rate limits page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with an address that isn't on your team.&lt;/strong&gt; A fresh Gmail works. Check spam too.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host:          smtp.yourprovider.com
Port:          587
Username:      from your provider
Password:      SMTP password or API key
Sender email:  no-reply@auth.yourdomain.com
Sender name:   Your App
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Don't "fix" it by turning off confirmation
&lt;/h2&gt;

&lt;p&gt;Sign-up will look fine, but password reset still sends email, and on the default service that only reaches your team. Supabase's changelog warns "the forgot password / reset password flow will be broken for your project." It also lets anyone register with an address they don't own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still not arriving?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Still "not authorized":&lt;/strong&gt; custom SMTP isn't enabled or didn't save.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Error sending confirmation email":&lt;/strong&gt; Supabase reached your SMTP server and got refused. The Auth logs show the provider's reply, usually bad credentials or an unverified From domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lands in spam:&lt;/strong&gt; finish SPF/DKIM/DMARC and keep Auth mail plain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link opens localhost or the preview:&lt;/strong&gt; that's your Site URL, not SMTP. I covered that in &lt;a href="https://dev.to/fixloopdev/lovable-login-works-in-preview-breaks-in-production-the-3-redirect-lists-4ip0"&gt;Lovable login works in preview, breaks in production&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full guide with the checklist and FAQ: &lt;a href="https://fixloop.dev/guides/supabase-email-address-not-authorized" rel="noopener noreferrer"&gt;fixloop.dev/guides/supabase-email-address-not-authorized&lt;/a&gt;. If you're stuck on it right now, paste the error at &lt;a href="https://fixloop.dev/start?type=diagnose&amp;amp;src=devto" rel="noopener noreferrer"&gt;fixloop.dev/start&lt;/a&gt; and we'll email the likely cause within 24 hours, free. (I run FixLoop.)&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>auth</category>
      <category>lovable</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Supabase select returns [] with no error: 5 RLS causes</title>
      <dc:creator>FixLoop</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:11:05 +0000</pubDate>
      <link>https://dev.to/fixloopdev/supabase-select-returns-with-no-error-5-rls-causes-1k46</link>
      <guid>https://dev.to/fixloopdev/supabase-select-returns-with-no-error-5-rls-causes-1k46</guid>
      <description>&lt;p&gt;You run &lt;code&gt;supabase.from("posts").select("*")&lt;/code&gt; and get &lt;code&gt;{ data: [], error: null }&lt;/code&gt;. The rows are in the table. The same query works in the SQL editor. Your app shows nothing.&lt;/p&gt;

&lt;p&gt;That's Row Level Security doing its job. RLS on with no matching SELECT policy filters every row out: zero rows, no error. The SQL editor runs as &lt;code&gt;postgres&lt;/code&gt;, which owns the tables and skips RLS, so it sees everything. Your app goes through the Data API as &lt;code&gt;anon&lt;/code&gt; or &lt;code&gt;authenticated&lt;/code&gt;, and those roles only see rows a policy allows.&lt;/p&gt;

&lt;p&gt;If you get &lt;code&gt;42501 permission denied for table&lt;/code&gt; instead, that's a missing GRANT, not this bug: &lt;a href="https://dev.to/fixloopdev/supabase-oct-30-why-new-tables-get-42501-and-how-to-fix-grants-rls-2i8k"&gt;the Oct 30 grants post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. RLS on, no SELECT policy
&lt;/h2&gt;

&lt;p&gt;From the Supabase RLS docs: "Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"anyone can read posts"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Policy targets the wrong role
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;to authenticated&lt;/code&gt; never matches a signed-out visitor, or a signed-in one whose session hasn't loaded yet. For public reads use &lt;code&gt;to anon, authenticated&lt;/code&gt;, or leave out &lt;code&gt;to&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;auth.uid()&lt;/code&gt; is null
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;using (auth.uid() = user_id)&lt;/code&gt; only works when the request carries the user's JWT. Without it, &lt;code&gt;null = user_id&lt;/code&gt; is never true, so every row disappears. Usual reasons: not signed in, querying on first render before the session is ready, or server code creating a client with only the anon key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uid:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// undefined = no session&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. The rows belong to someone else
&lt;/h2&gt;

&lt;p&gt;The policy is right; the data isn't. Check in the SQL editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;user_id&lt;/code&gt; null or another account's id means the insert path is wrong, not the policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Policy checks the wrong column
&lt;/h2&gt;

&lt;p&gt;AI-generated schemas sometimes call the owner column &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;owner_id&lt;/code&gt;, or reuse &lt;code&gt;id&lt;/code&gt;. A policy comparing &lt;code&gt;auth.uid()&lt;/code&gt; to the row's primary key matches nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm it in two minutes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;polname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qual&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'posts'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No row with &lt;code&gt;cmd = SELECT&lt;/code&gt;? That's cause 1. Then log &lt;code&gt;user?.id&lt;/code&gt; right before the query. If it's undefined, fix auth first.&lt;/p&gt;

&lt;p&gt;For owner-only data, this is the shape the Supabase docs use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"owners can read their posts"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't fix it by disabling RLS. That hands every row to anyone holding your publishable key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full guide
&lt;/h2&gt;

&lt;p&gt;Longer version with the debug policy trick, the empty-array vs 42501 difference, and an FAQ:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fixloop.dev/guides/supabase-select-returns-empty-array" rel="noopener noreferrer"&gt;https://fixloop.dev/guides/supabase-select-returns-empty-array&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I run &lt;a href="https://fixloop.dev" rel="noopener noreferrer"&gt;FixLoop&lt;/a&gt;. Still stuck? &lt;a href="https://fixloop.dev/start?type=diagnose&amp;amp;src=devto" rel="noopener noreferrer"&gt;Paste the error&lt;/a&gt; and we'll email you the likely cause, free.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>lovable</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Lovable app not deploying or broken after deploy: 4 env causes</title>
      <dc:creator>FixLoop</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:12:41 +0000</pubDate>
      <link>https://dev.to/fixloopdev/lovable-app-not-deploying-or-broken-after-deploy-4-env-causes-2g4a</link>
      <guid>https://dev.to/fixloopdev/lovable-app-not-deploying-or-broken-after-deploy-4-env-causes-2g4a</guid>
      <description>&lt;p&gt;Preview works. You publish, or point Cloudflare Pages / Netlify at the GitHub repo, and the live site is blank, says &lt;code&gt;Failed to fetch&lt;/code&gt;, or the build dies. You didn't touch the React. The environment changed.&lt;/p&gt;

&lt;p&gt;Lovable preview reads your committed &lt;code&gt;.env&lt;/code&gt; and injects your Cloud secrets for you. An external host does neither unless you copy the values into its &lt;strong&gt;Production&lt;/strong&gt; env and rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Preview loads data. Production is empty or spins forever.&lt;/li&gt;
&lt;li&gt;The deploy is green, but DevTools &amp;gt; Network on the live site hits &lt;code&gt;undefined.supabase.co&lt;/code&gt;, &lt;code&gt;localhost&lt;/code&gt;, or your preview project.&lt;/li&gt;
&lt;li&gt;The build log complains about a missing env var, or &lt;code&gt;npm run build&lt;/code&gt; fails on a secret a plugin expected.&lt;/li&gt;
&lt;li&gt;Edge Functions work in preview and 500 on the live host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test in a private window on the live hostname. Preview cookies and preview env will lie to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;VITE_&lt;/code&gt; vars set on Preview, not Production
&lt;/h2&gt;

&lt;p&gt;Cloudflare Pages and Netlify keep separate Preview and Production env. A common miss: paste &lt;code&gt;VITE_SUPABASE_URL&lt;/code&gt; and the publishable key into Preview, check the preview URL, never open the Production tab.&lt;/p&gt;

&lt;p&gt;Also: if you gitignored &lt;code&gt;.env&lt;/code&gt; "for security," Lovable can keep previewing from its copy while the Git-connected host builds with nothing. Put the &lt;code&gt;VITE_*&lt;/code&gt; values in the host's Production env (they're public browser values anyway). Never put backend secrets there.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Secrets that never left Lovable
&lt;/h2&gt;

&lt;p&gt;Lovable Secrets (&lt;code&gt;STRIPE_SECRET_KEY&lt;/code&gt;, &lt;code&gt;RESEND_API_KEY&lt;/code&gt;, webhook signing secrets) are injected into Lovable Cloud functions. They're not in git, so your new host or your own Supabase project doesn't have them. Copy every name the server code reads into the host's secrets (or Supabase Edge Function secrets). A function that 500s only in production with a missing-env line in its log is this one.&lt;/p&gt;

&lt;p&gt;The opposite mistake: putting &lt;code&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/code&gt; in a &lt;code&gt;VITE_&lt;/code&gt; var to "make it work." That ships it in your JS bundle. That's a leak, not a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Changing env without rebuilding
&lt;/h2&gt;

&lt;p&gt;This is the one that wastes the most time. From the Vite docs, &lt;code&gt;import.meta.env&lt;/code&gt; values are "statically replaced at build time." The live JS already contains the old string (or &lt;code&gt;undefined&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_SUPABASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// after `vite build` this is a string literal,&lt;/span&gt;
&lt;span class="c1"&gt;// or undefined, which turns into a broken request URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So saving a new value in the Pages / Netlify UI and refreshing does nothing. Retry the production deployment (or push a commit) so the build runs again. Also, &lt;code&gt;process.env.VITE_*&lt;/code&gt; in client code is the wrong API; Vite uses &lt;code&gt;import.meta.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Quick check: search the built JS for your Supabase project ref. If it isn't there, the build never saw the var.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. URL and key from different projects
&lt;/h2&gt;

&lt;p&gt;Production has the URL from project A and the anon key from project B, or an old key after a rotation. The client "connects," but auth, rows and RLS belong to a different project. Names drift too: older templates read &lt;code&gt;VITE_SUPABASE_ANON_KEY&lt;/code&gt;, current Lovable &lt;code&gt;.env&lt;/code&gt; uses &lt;code&gt;VITE_SUPABASE_PUBLISHABLE_KEY&lt;/code&gt;. If Production only has the old name, &lt;code&gt;createClient&lt;/code&gt; gets &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Private window, live hostname, DevTools &amp;gt; Network: which host do &lt;code&gt;/rest/v1&lt;/code&gt; calls hit? Same as preview?&lt;/li&gt;
&lt;li&gt;Host env, &lt;strong&gt;Production&lt;/strong&gt; tab: &lt;code&gt;VITE_SUPABASE_URL&lt;/code&gt; + publishable key, same project.&lt;/li&gt;
&lt;li&gt;After any env change, rebuild. Don't just refresh.&lt;/li&gt;
&lt;li&gt;Search the bundle for your project ref.&lt;/li&gt;
&lt;li&gt;Every secret your functions read exists on the live host or in Supabase secrets.&lt;/li&gt;
&lt;li&gt;Build failed? The log names the variable. Set it on Production, rebuild.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VITE_SUPABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://YOUR_PROJECT.supabase.co
&lt;span class="nv"&gt;VITE_SUPABASE_PUBLISHABLE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eyJ...   &lt;span class="c"&gt;# anon/publishable, never service_role&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If deploy is fine but &lt;strong&gt;login&lt;/strong&gt; breaks on the live domain, that's the redirect allowlists, not env: &lt;a href="https://dev.to/fixloopdev/lovable-login-works-in-preview-breaks-in-production-the-3-redirect-lists-4ip0"&gt;the 3 redirect lists&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full guide
&lt;/h2&gt;

&lt;p&gt;Longer version with sources and an FAQ (Lovable Secrets vs &lt;code&gt;VITE_&lt;/code&gt;, build failures, preview and production on different projects):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fixloop.dev/guides/lovable-preview-works-deploy-breaks" rel="noopener noreferrer"&gt;https://fixloop.dev/guides/lovable-preview-works-deploy-breaks&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I run &lt;a href="https://fixloop.dev" rel="noopener noreferrer"&gt;FixLoop&lt;/a&gt;. Still stuck? &lt;a href="https://fixloop.dev/start?type=diagnose&amp;amp;src=devto" rel="noopener noreferrer"&gt;Paste the error&lt;/a&gt; and we'll email you the likely cause, free.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vite</category>
      <category>lovable</category>
      <category>supabase</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Stripe webhook 401 on Supabase Edge Functions: verify_jwt is blocking it</title>
      <dc:creator>FixLoop</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:04:34 +0000</pubDate>
      <link>https://dev.to/fixloopdev/stripe-webhook-401-on-supabase-edge-functions-verifyjwt-is-blocking-it-2g32</link>
      <guid>https://dev.to/fixloopdev/stripe-webhook-401-on-supabase-edge-functions-verifyjwt-is-blocking-it-2g32</guid>
      <description>&lt;p&gt;Checkout works. Stripe's dashboard shows the webhook failing with &lt;strong&gt;401&lt;/strong&gt;. Your Edge Function logs are empty for that timestamp. That combo is almost never your Stripe code. It's the Supabase JWT check running before your handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;In Stripe Workbench, open the endpoint and look at a failed delivery. The response body comes from the platform, not from your &lt;code&gt;return new Response(...)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Missing authorization header"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes it's &lt;code&gt;Invalid JWT&lt;/code&gt; instead. Same thing. Other tells:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No invocation in the function logs for that delivery.&lt;/li&gt;
&lt;li&gt;The function works when you call it yourself with &lt;code&gt;Authorization: Bearer &amp;lt;anon key&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Local &lt;code&gt;stripe listen&lt;/code&gt; + &lt;code&gt;supabase functions serve --no-verify-jwt&lt;/code&gt; works, production 401s.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Every Edge Function request goes through two layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;verify_jwt&lt;/code&gt;&lt;/strong&gt; (on by default). The platform wants a Supabase JWT in &lt;code&gt;Authorization&lt;/code&gt;. Missing or wrong means 401 and your code never runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your handler.&lt;/strong&gt; This is where Stripe's signature check belongs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stripe sends &lt;code&gt;Stripe-Signature&lt;/code&gt; and a JSON body. It has no Supabase token, so layer 1 always rejects it. Supabase's own function config docs use a Stripe webhook as the example for turning this off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: turn off JWT verification for that one function
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;supabase/config.toml&lt;/code&gt; (use the function's folder name):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[functions.stripe-webhook]&lt;/span&gt;
&lt;span class="py"&gt;verify_jwt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redeploy, or &lt;code&gt;supabase functions deploy stripe-webhook --no-verify-jwt&lt;/code&gt;. You can also flip the toggle in the dashboard, but if Lovable redeploys the function, a dashboard-only toggle can come back on. Put it in config so it sticks. Leave JWT verification on for every user-facing function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: verify the signature on the raw body
&lt;/h2&gt;

&lt;p&gt;Now anyone can POST to that URL, so the handler has to prove it's Stripe. Two gotchas on Deno:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the body with &lt;code&gt;req.text()&lt;/code&gt; first. Stripe's docs: "Stripe requires the raw body of the request to perform signature verification." &lt;code&gt;req.json()&lt;/code&gt; before verifying will break it.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;constructEventAsync&lt;/code&gt;, not &lt;code&gt;constructEvent&lt;/code&gt;. Deno's crypto is async.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm:stripe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cryptoProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSubtleCryptoProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stripe-Signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEventAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;cryptoProvider&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stripe signature verification failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bad signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// handle event.type, then acknowledge fast&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Return 400 on a bad signature (not 401, or you'll think the JWT bug is back) and 200 quickly once you've accepted the event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The secret mix-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Production deliveries are signed with the endpoint secret from Stripe Workbench (starts with &lt;code&gt;whsec_&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stripe listen&lt;/code&gt; prints a &lt;strong&gt;different&lt;/strong&gt; &lt;code&gt;whsec_&lt;/code&gt;. Don't mix them.&lt;/li&gt;
&lt;li&gt;Test mode and live mode each have their own.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;STRIPE_SECRET_KEY&lt;/code&gt; and &lt;code&gt;STRIPE_WEBHOOK_SECRET&lt;/code&gt; in Edge Function secrets, never in &lt;code&gt;VITE_&lt;/code&gt; client env.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;401 body says &lt;code&gt;Missing authorization header&lt;/code&gt; or &lt;code&gt;Invalid JWT&lt;/code&gt;? JWT gate. Set &lt;code&gt;verify_jwt = false&lt;/code&gt; on that function only.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;req.text()&lt;/code&gt;, then &lt;code&gt;constructEventAsync(...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;400 on bad signature, 200 when accepted.&lt;/li&gt;
&lt;li&gt;Dashboard &lt;code&gt;whsec_&lt;/code&gt; in prod, CLI secret only with &lt;code&gt;stripe listen&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Full guide
&lt;/h2&gt;

&lt;p&gt;Longer version with source links and an FAQ ("does turning off verify_jwt make it public?", constructEvent vs constructEventAsync):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fixloop.dev/guides/stripe-webhook-401-supabase-edge-functions" rel="noopener noreferrer"&gt;https://fixloop.dev/guides/stripe-webhook-401-supabase-edge-functions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the webhook "succeeds" but the row never changes, that's usually RLS silently blocking the update, not Stripe. If it's login that broke after deploy, I wrote up &lt;a href="https://dev.to/fixloopdev/lovable-login-works-in-preview-breaks-in-production-the-3-redirect-lists-4ip0"&gt;the 3 redirect lists&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I run &lt;a href="https://fixloop.dev" rel="noopener noreferrer"&gt;FixLoop&lt;/a&gt;. There's a &lt;a href="https://fixloop.dev/scan" rel="noopener noreferrer"&gt;free scan&lt;/a&gt; for leaked keys and missing headers, and &lt;a href="https://fixloop.dev/start?type=diagnose&amp;amp;src=devto" rel="noopener noreferrer"&gt;we fix broken Lovable/Supabase apps&lt;/a&gt; for a fixed price. No fix, no pay.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>stripe</category>
      <category>webhooks</category>
      <category>lovable</category>
    </item>
    <item>
      <title>Lovable login works in preview, breaks in production: the 3 redirect lists</title>
      <dc:creator>FixLoop</dc:creator>
      <pubDate>Sun, 18 Oct 2026 17:04:15 +0000</pubDate>
      <link>https://dev.to/fixloopdev/lovable-login-works-in-preview-breaks-in-production-the-3-redirect-lists-4ip0</link>
      <guid>https://dev.to/fixloopdev/lovable-login-works-in-preview-breaks-in-production-the-3-redirect-lists-4ip0</guid>
      <description>&lt;p&gt;Login works on the Lovable preview. You publish, or put a custom domain in front, and now sign-in loops, drops you on &lt;code&gt;localhost:3000&lt;/code&gt;, or comes back with no session. The React didn't change. The host did.&lt;/p&gt;

&lt;p&gt;Supabase Auth only sends the browser back to URLs it already trusts. Preview was on that list. Production wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Preview: Google / GitHub / magic link works. Production: spinner, then the login page again.&lt;/li&gt;
&lt;li&gt;After OAuth you land on &lt;code&gt;http://localhost:3000&lt;/code&gt; or the old &lt;code&gt;*.lovable.app&lt;/code&gt; preview host.&lt;/li&gt;
&lt;li&gt;The address bar ends in &lt;code&gt;#error=...&lt;/code&gt; or &lt;code&gt;error_description=Unsupported%20redirect&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Google shows &lt;code&gt;Error 400: redirect_uri_mismatch&lt;/code&gt; naming a URL you don't recognize.&lt;/li&gt;
&lt;li&gt;Confirm / reset emails open the preview app, or 404 on the live domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test in a private window on the live origin. Preview cookies will lie to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are three lists, not one
&lt;/h2&gt;

&lt;p&gt;Most people paste the production URL into one box and call it done.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Supabase Site URL&lt;/strong&gt; (Authentication → URL Configuration). The default redirect when your code doesn't pass &lt;code&gt;redirectTo&lt;/code&gt;. Magic links and password resets use it. A new project starts at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase Redirect URLs&lt;/strong&gt; (same page). The allowlist. Every &lt;code&gt;redirectTo&lt;/code&gt; must match it, or Auth quietly ignores it and uses Site URL. That's how you "set production" and still bounce to localhost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The OAuth provider's Authorized redirect URI&lt;/strong&gt; (Google Cloud, GitHub). This is &lt;em&gt;not&lt;/em&gt; your app. It's Supabase's callback, &lt;code&gt;https://YOUR_PROJECT.supabase.co/auth/v1/callback&lt;/code&gt;. Publishing the Lovable app doesn't change it. Getting it wrong gives &lt;code&gt;redirect_uri_mismatch&lt;/code&gt; at the provider, which looks like a Lovable bug.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On a connected Supabase project, the first two live in the Supabase dashboard, not the Lovable chat. (Lovable Cloud, the built-in backend, is configured in Lovable instead.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Site URL&lt;/strong&gt; = the live origin, e.g. &lt;code&gt;https://myapp.com&lt;/code&gt; (or your &lt;code&gt;*.lovable.app&lt;/code&gt; host if that's what's published). Leave preview out of it, since it's the default for emails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirect URLs&lt;/strong&gt;: add every origin you actually use. &lt;code&gt;**&lt;/code&gt; matches across &lt;code&gt;/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://myapp.com/**
https://myapp.com/auth/callback
https://*.lovable.app/**
http://localhost:5173/**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever path your code passes as &lt;code&gt;redirectTo&lt;/code&gt; must be covered. A trailing-slash mismatch is enough to fall back to Site URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider console&lt;/strong&gt; (Google / GitHub): JavaScript origins get your app's origin. Authorized redirect URIs get the Supabase callback copied from Authentication → Providers. Don't put &lt;code&gt;https://myapp.com/auth/callback&lt;/code&gt; there: Google returns to Supabase, and Supabase returns to your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;redirectTo in code&lt;/strong&gt;: hardcoding the preview host is how production "randomly" returns to preview. Use the current origin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/callback`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithOAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redirectTo&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same for &lt;code&gt;resetPasswordForEmail&lt;/code&gt; and &lt;code&gt;signInWithOtp&lt;/code&gt;. If you customized email templates, &lt;code&gt;{{ .SiteURL }}&lt;/code&gt; always points at Site URL; use &lt;code&gt;{{ .RedirectTo }}&lt;/code&gt; when the link should follow what you passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  60-second checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Private window, live hostname.&lt;/li&gt;
&lt;li&gt;Site URL is production, not localhost or only the preview host.&lt;/li&gt;
&lt;li&gt;Redirect URLs cover production (plus &lt;code&gt;/auth/callback&lt;/code&gt; if you use it); preview can stay as a wildcard.&lt;/li&gt;
&lt;li&gt;Provider redirect URI is &lt;code&gt;https://YOUR_PROJECT.supabase.co/auth/v1/callback&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redirectTo&lt;/code&gt; is built from &lt;code&gt;window.location.origin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Read the &lt;code&gt;error&lt;/code&gt; / &lt;code&gt;error_description&lt;/code&gt; in the URL fragment on the landing page. Auth still redirects on failure; the reason is in the hash.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Full guide
&lt;/h2&gt;

&lt;p&gt;The longer version with source links and an FAQ (Site URL vs Redirect URLs, "I set Site URL, why localhost?", Lovable vs Supabase settings) is here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fixloop.dev/guides/lovable-login-preview-vs-production" rel="noopener noreferrer"&gt;https://fixloop.dev/guides/lovable-login-preview-vs-production&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If sessions exist but saves silently do nothing, that's RLS, not redirects. I wrote up a &lt;a href="https://dev.to/fixloopdev/is-my-supabase-database-public-a-5-minute-check-with-the-anon-key-2iil"&gt;5-minute check for that&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I run &lt;a href="https://fixloop.dev" rel="noopener noreferrer"&gt;FixLoop&lt;/a&gt;. There's a &lt;a href="https://fixloop.dev/scan" rel="noopener noreferrer"&gt;free scan&lt;/a&gt; for leaked keys and missing headers, and &lt;a href="https://fixloop.dev/start?type=diagnose&amp;amp;src=devto" rel="noopener noreferrer"&gt;we fix broken Lovable/Supabase apps&lt;/a&gt; for a fixed price. No fix, no pay.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>lovable</category>
      <category>webdev</category>
      <category>auth</category>
    </item>
    <item>
      <title>Is my Supabase database public? A 5-minute check with the anon key</title>
      <dc:creator>FixLoop</dc:creator>
      <pubDate>Sat, 17 Oct 2026 20:09:09 +0000</pubDate>
      <link>https://dev.to/fixloopdev/is-my-supabase-database-public-a-5-minute-check-with-the-anon-key-2iil</link>
      <guid>https://dev.to/fixloopdev/is-my-supabase-database-public-a-5-minute-check-with-the-anon-key-2iil</guid>
      <description>&lt;p&gt;If you shipped a Lovable (or any Supabase) app, your anon key is in the browser. That's fine, it's supposed to be. The real question is different: &lt;strong&gt;can a stranger read your tables with that key and no login?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If Row Level Security is off on a table that still has grants to &lt;code&gt;anon&lt;/code&gt;, the answer is yes. Here's a 5-minute check you can run on your own project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the anon key isn't the leak
&lt;/h2&gt;

&lt;p&gt;The anon key is a public JWT. Postgres then decides what it can do in two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grants&lt;/strong&gt; decide whether &lt;code&gt;anon&lt;/code&gt; can SELECT / INSERT / UPDATE / DELETE the table at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RLS&lt;/strong&gt; decides which rows survive that grant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RLS off means the second layer never runs. If &lt;code&gt;anon&lt;/code&gt; also has a grant (the old default on &lt;code&gt;public&lt;/code&gt; tables), every row is reachable from the REST endpoint. A policy of &lt;code&gt;using (true)&lt;/code&gt; is the same hole with RLS "on": the dashboard badge looks fine, the table is still public.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check
&lt;/h2&gt;

&lt;p&gt;Open your live app in a private window, DevTools → Network, filter &lt;code&gt;rest/v1&lt;/code&gt;. Grab the project host (&lt;code&gt;abcdefghij.supabase.co&lt;/code&gt;), a table name from the path, and the &lt;code&gt;apikey&lt;/code&gt; header from a signed-out request. &lt;strong&gt;Never use the &lt;code&gt;service_role&lt;/code&gt; key&lt;/strong&gt; for this (or anywhere in the client).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://YOUR_PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*&amp;amp;limit=5"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: YOUR_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_ANON_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No cookies, no session. If this returns user data, anyone can do it too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200 + rows&lt;/strong&gt;: anon can read the table. RLS is off or a policy allows anonymous SELECT. Fix this first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;200 + &lt;code&gt;[]&lt;/code&gt;&lt;/strong&gt;: there's a SELECT grant but no rows passed RLS (or the table is empty). Retry on a table you know has data. If the signed-in app shows rows and anon still gets &lt;code&gt;[]&lt;/code&gt;, you're probably fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;401&lt;/strong&gt;: bad or truncated key. This does &lt;em&gt;not&lt;/em&gt; mean the table is private.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;403 / 42501&lt;/strong&gt;: anon has no grant, so the request never reaches RLS. After Oct 30, 2026 this shows up a lot on new tables (&lt;a href="https://dev.to/fixloopdev/supabase-oct-30-why-new-tables-get-42501-and-how-to-fix-grants-rls-2i8k"&gt;why&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;404 / PGRST205&lt;/strong&gt;: wrong table name. Use the one from the network tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The safe fix
&lt;/h2&gt;

&lt;p&gt;Turn RLS on and scope policies to &lt;code&gt;auth.uid()&lt;/code&gt;. Don't "fix" an open table by granting more.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"select own rows"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"insert own rows"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't do any of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GRANT ALL ON ALL TABLES IN SCHEMA public TO anon;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;any grant to anon while RLS is off&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;using (true)&lt;/code&gt; on a table with private rows&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;service_role&lt;/code&gt; key in frontend env&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Lovable, put the same rules in your project instructions so the next migration doesn't recreate the hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full guide
&lt;/h2&gt;

&lt;p&gt;The longer version with the browser-console variant, a checklist, and an FAQ (should I rotate the anon key? Lovable says RLS is on, can it still be public?) is here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fixloop.dev/guides/is-my-supabase-database-public" rel="noopener noreferrer"&gt;https://fixloop.dev/guides/is-my-supabase-database-public&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I run &lt;a href="https://fixloop.dev" rel="noopener noreferrer"&gt;FixLoop&lt;/a&gt;. There's a &lt;a href="https://fixloop.dev/scan" rel="noopener noreferrer"&gt;free scan&lt;/a&gt; for leaked keys and missing headers, and &lt;a href="https://fixloop.dev/start?type=diagnose&amp;amp;src=devto" rel="noopener noreferrer"&gt;we fix broken Lovable/Supabase apps&lt;/a&gt; for a fixed price. No fix, no pay.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>webdev</category>
      <category>lovable</category>
    </item>
    <item>
      <title>Supabase Oct 30: why new tables get 42501 (and how to fix grants + RLS)</title>
      <dc:creator>FixLoop</dc:creator>
      <pubDate>Sat, 17 Oct 2026 17:14:20 +0000</pubDate>
      <link>https://dev.to/fixloopdev/supabase-oct-30-why-new-tables-get-42501-and-how-to-fix-grants-rls-2i8k</link>
      <guid>https://dev.to/fixloopdev/supabase-oct-30-why-new-tables-get-42501-and-how-to-fix-grants-rls-2i8k</guid>
      <description>&lt;p&gt;If you just created a table in Lovable, turned on RLS, wrote policies, and the app still returns &lt;code&gt;permission denied for table&lt;/code&gt;, this is the one.&lt;/p&gt;

&lt;p&gt;Starting &lt;strong&gt;October 30, 2026&lt;/strong&gt;, existing Supabase projects stop auto-granting privileges on new tables in the &lt;code&gt;public&lt;/code&gt; schema. The table is there. The policies are there. The Data API still cannot see it.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;Supabase changelog&lt;/a&gt; · &lt;a href="https://supabase.com/docs/guides/api/securing-your-api" rel="noopener noreferrer"&gt;Securing your API&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;The request fails with Postgres error &lt;strong&gt;42501&lt;/strong&gt;. PostgREST (the Data API behind &lt;code&gt;supabase-js&lt;/code&gt;) sends something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"42501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"permission denied for table your_table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Grant the required privileges to the current role with: GRANT SELECT ON public.your_table TO anon;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical setup: a Lovable-created table, RLS enabled, policies that should allow the row, and it still fails. Old tables keep working. Only the new one fails.&lt;/p&gt;

&lt;p&gt;The hint names the missing grant. It is &lt;strong&gt;not&lt;/strong&gt; a recommendation to copy blindly. Read it, then grant only what that role should have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grants vs RLS
&lt;/h2&gt;

&lt;p&gt;Postgres runs two checks, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grants&lt;/strong&gt; decide whether a role can touch the table at all (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row Level Security&lt;/strong&gt; decides which rows that role can see or change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the grant is missing, RLS never runs. That is why "I have policies" does not fix 42501.&lt;/p&gt;

&lt;p&gt;Until now, a new table in &lt;code&gt;public&lt;/code&gt; automatically received select/insert/update/delete for &lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;authenticated&lt;/code&gt;, and &lt;code&gt;service_role&lt;/code&gt;. RLS was the only gate. New projects already dropped that default. On October 30 it applies to every existing project too.&lt;/p&gt;

&lt;p&gt;Existing tables keep their grants. Direct Postgres connections are not affected. This is a Data API change: REST, GraphQL, and the client libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Put grants in the same migration as the table, RLS, and policies. Least privilege, not &lt;code&gt;GRANT ALL&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- signed-in users: the operations your app actually uses&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- server code / Edge Functions using the service_role key&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- only if signed-out visitors should read this table&lt;/span&gt;
&lt;span class="c1"&gt;-- grant select on table public.your_table to anon;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave &lt;code&gt;anon&lt;/code&gt; with nothing unless the table is meant to be public. Skip &lt;code&gt;GRANT ALL ON ALL TABLES IN SCHEMA public&lt;/code&gt; - that also opens tables where RLS was never turned on.&lt;/p&gt;

&lt;p&gt;If you already see 42501, check who has what:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;privilege_type&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role_table_grants&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
 &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your_table'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tell Lovable to include grants
&lt;/h2&gt;

&lt;p&gt;Paste this into project instructions / Knowledge before the next migration:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you create or change a Supabase table, put all of this in the same migration: CREATE TABLE, ENABLE ROW LEVEL SECURITY, CREATE POLICY, GRANT to authenticated and service_role, and GRANT SELECT to anon only if signed-out users should read it. Do not use GRANT ALL. Do not skip grants because RLS is on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Full checklist + FAQ
&lt;/h2&gt;

&lt;p&gt;The longer version with the Lovable paste block, checklist, and FAQ lives here (canonical):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fixloop.dev/guides/supabase-oct-30-grants-change" rel="noopener noreferrer"&gt;https://fixloop.dev/guides/supabase-oct-30-grants-change&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I run &lt;a href="https://fixloop.dev" rel="noopener noreferrer"&gt;FixLoop&lt;/a&gt; - &lt;a href="https://fixloop.dev/start?type=diagnose&amp;amp;src=devto" rel="noopener noreferrer"&gt;we fix broken Lovable/Supabase apps&lt;/a&gt; for a fixed price. No fix, no pay.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>webdev</category>
      <category>security</category>
      <category>lovable</category>
    </item>
  </channel>
</rss>
