<?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: PatchAndShip</title>
    <description>The latest articles on DEV Community by PatchAndShip (@patchandship).</description>
    <link>https://dev.to/patchandship</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%2F4207904%2Fe5ba3fda-d46e-4b87-ae12-1e6f97e90cc3.png</url>
      <title>DEV Community: PatchAndShip</title>
      <link>https://dev.to/patchandship</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/patchandship"/>
    <language>en</language>
    <item>
      <title>Free check walkthrough for the first 20 Lovable, Bolt, Replit or Cursor apps</title>
      <dc:creator>PatchAndShip</dc:creator>
      <pubDate>Sun, 01 Nov 2026 20:52:50 +0000</pubDate>
      <link>https://dev.to/patchandship/free-check-walkthrough-for-the-first-20-lovable-bolt-replit-or-cursor-apps-412p</link>
      <guid>https://dev.to/patchandship/free-check-walkthrough-for-the-first-20-lovable-bolt-replit-or-cursor-apps-412p</guid>
      <description>&lt;p&gt;If you built an app with Lovable, Bolt, Replit or Cursor and it runs on Supabase, a few settings may never have had a second look. Row Level Security might be off on a table, a key might be sitting in your JavaScript, or a storage bucket might be public when you didn't mean it to be.&lt;/p&gt;

&lt;p&gt;So we're offering a free personal walkthrough to the first 20 people who ask. Once 20 people have claimed it, it ends.&lt;/p&gt;

&lt;p&gt;We run the instant check on your app and walk you through what it found in plain words: any keys or secrets sitting in your JavaScript, which security headers are missing, and any public storage links in your files. It only sees what a visitor's browser already gets. It doesn't query your database or look at your RLS rules, so we'll tell you plainly what it can't see.&lt;/p&gt;

&lt;p&gt;It's free, there's nothing to buy, and it's one walkthrough per app or founder.&lt;/p&gt;

&lt;p&gt;To ask for one, run the free instant check on your app first. It needs no signup and only looks at what any visitor's browser already gets. Then email &lt;a href="mailto:patchandship@gmail.com"&gt;patchandship@gmail.com&lt;/a&gt; with your app's link and "first 20" in the subject.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://patchandship.com/check.html?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=first-20" rel="noopener noreferrer"&gt;https://patchandship.com/check.html?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=first-20&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Got a question about your own setup? Leave it in the comments and we'll answer there.&lt;/p&gt;

&lt;p&gt;Not affiliated with Lovable, Bolt, Replit or Cursor.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>webdev</category>
      <category>security</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Your app worked, then one deploy broke it. Where do you look first?</title>
      <dc:creator>PatchAndShip</dc:creator>
      <pubDate>Sun, 01 Nov 2026 19:27:08 +0000</pubDate>
      <link>https://dev.to/patchandship/your-app-worked-then-one-deploy-broke-it-where-do-you-look-first-383d</link>
      <guid>https://dev.to/patchandship/your-app-worked-then-one-deploy-broke-it-where-do-you-look-first-383d</guid>
      <description>&lt;p&gt;A common one with Lovable and Bolt apps: it was live and fine, then a small change and a new deploy, and now it's a blank page, a 404 on refresh, or a login loop.&lt;/p&gt;

&lt;p&gt;Often the code isn't the problem. Something outside it didn't come along:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;VITE_&lt;/code&gt; env vars are baked in at build time, so set them on the host and redeploy.&lt;/li&gt;
&lt;li&gt;Supabase's Site URL and Redirect URLs may still point at an old address.&lt;/li&gt;
&lt;li&gt;Refreshing a page gives a 404? The host often needs a rewrite to index.html.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the last thing that broke after a deploy for you?&lt;/p&gt;

&lt;p&gt;If you're stuck on one, App Rescue is $149 for one issue: &lt;a href="https://patchandship.com/app-rescue?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=app-rescue" rel="noopener noreferrer"&gt;https://patchandship.com/app-rescue?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=app-rescue&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>lovable</category>
      <category>supabase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Moving off Lovable Cloud to your own Supabase project</title>
      <dc:creator>PatchAndShip</dc:creator>
      <pubDate>Sun, 01 Nov 2026 18:51:11 +0000</pubDate>
      <link>https://dev.to/patchandship/moving-off-lovable-cloud-to-your-own-supabase-project-1lbd</link>
      <guid>https://dev.to/patchandship/moving-off-lovable-cloud-to-your-own-supabase-project-1lbd</guid>
      <description>&lt;p&gt;If your app runs on Lovable Cloud and you want your database in a Supabase account you own, you can move it. Lovable doesn't do it in one step, though. Here's what changes, what you keep, and a quick way to check the move worked before you remove anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why people move
&lt;/h2&gt;

&lt;p&gt;It usually comes down to three things. You want your database in a Supabase account you own and control. You want to see and manage your own usage and billing. And you want room to keep building with other tools, not only inside Lovable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you keep
&lt;/h2&gt;

&lt;p&gt;Lovable can export your database, including your users' accounts. You start the export under More → Database → Manage → Settings → Export data → Export → Start export, then download it from More → Storage.&lt;/p&gt;

&lt;p&gt;Then you restore that export into your new Supabase project by running a few restore commands on your own computer, with your own database password.&lt;/p&gt;

&lt;p&gt;Your users keep their passwords, so nobody has to reset anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes
&lt;/h2&gt;

&lt;p&gt;The export is a copy of your data and users. It isn't a working app on its own. A few things don't come with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Files, secrets, sign-in settings and edge functions.&lt;/strong&gt; These have to be set up again in the new project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessions.&lt;/strong&gt; Anyone who's signed in will need to sign in again, once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your app's connection.&lt;/strong&gt; Your env vars (in a Lovable app, usually &lt;code&gt;VITE_SUPABASE_URL&lt;/code&gt; and &lt;code&gt;VITE_SUPABASE_PUBLISHABLE_KEY&lt;/code&gt;) still point at the old backend until you change them to the new project's URL and public publishable (or legacy anon) key, then rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth URLs.&lt;/strong&gt; In your new project's Supabase Auth settings, update the Site URL and the redirect URLs to match your live app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google sign-in, if you use it.&lt;/strong&gt; In Google Cloud, add your new Supabase callback URL under &lt;em&gt;Authorized redirect URIs&lt;/em&gt;, and your live domain under &lt;em&gt;Authorized JavaScript origins&lt;/em&gt;. Then turn on the Google provider in the new project and paste in your Google client ID and secret.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the full job is: export, restore, check, reconnect sign-in, and point your app at the new project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't remove Lovable Cloud yet
&lt;/h2&gt;

&lt;p&gt;Leave your Lovable Cloud backend alone until you've checked everything on the new project. Using &lt;strong&gt;Remove database&lt;/strong&gt; in Lovable (More → Database → Manage → Settings) permanently deletes the database, its backups, your app's user accounts and your storage files, and it can't be undone. Keep your export file too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 3-step self-check
&lt;/h2&gt;

&lt;p&gt;Before you call the move done, run through these on your live app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data and rules.&lt;/strong&gt; Open the new project in Supabase and look at your tables. Are the rows there? Are your row level security policies there on the same tables as before? Then sign in as a normal user and make sure you see your own data, and not anyone else's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign-in.&lt;/strong&gt; Sign in with an existing user's email and password. If you use Google, sign in with Google too and make sure it sends you back to your live app, not to a localhost or preview URL. If it bounces, look at the Site URL, the redirect URLs and the Google settings above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app is really on the new project.&lt;/strong&gt; Open your live app with the browser dev tools on the Network tab and check that requests go to the new project's Supabase URL. Upload and open a file if your app uses storage, and trigger anything that runs an edge function.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all three pass, your app is running on a Supabase project you own. Only then think about removing the old backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you'd rather not do it yourself
&lt;/h2&gt;

&lt;p&gt;If you'd like it handled, we offer this move: you run the restore on your own computer with our step-by-step guide, and we never see your database password. We set the rest up with you and point your app at the new project. Auth users come over on every size, storage files on Medium and Large, and up to 10 edge functions on Large. You pick a size and send a request through the page. Details are here: &lt;a href="https://patchandship.com/lovable-cloud-to-supabase?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=cloud-move" rel="noopener noreferrer"&gt;Lovable Cloud to Supabase&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions about your own move? Ask in the comments.&lt;/p&gt;

&lt;p&gt;Want the next guide by email? Sign up for our newsletter, Preview to Prod: &lt;a href="https://patchandship.com/newsletter?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=newsletter" rel="noopener noreferrer"&gt;patchandship.com/newsletter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>lovable</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Two common breaks in Lovable and Bolt apps (and when to hand them off)</title>
      <dc:creator>PatchAndShip</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:35:46 +0000</pubDate>
      <link>https://dev.to/patchandship/two-common-breaks-in-lovable-and-bolt-apps-and-when-to-hand-them-off-3721</link>
      <guid>https://dev.to/patchandship/two-common-breaks-in-lovable-and-bolt-apps-and-when-to-hand-them-off-3721</guid>
      <description>&lt;p&gt;Two problems come up a lot with apps built in Lovable, Bolt, Replit and Cursor, so we put up a page for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. It works in preview, then breaks once it's live
&lt;/h2&gt;

&lt;p&gt;Often it's a small setting, not your code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supabase Site URL&lt;/strong&gt; is the default place people land after sign-in when your app doesn't pass a &lt;code&gt;redirectTo&lt;/code&gt;. If it still points at the preview domain, that's where they go. Set it to your live domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect URLs&lt;/strong&gt; is the allow list. Add your live domain, like &lt;code&gt;https://yourapp.com/**&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google sign-in:&lt;/strong&gt; your live domain goes under Authorized JavaScript origins, and the redirect URI is &lt;code&gt;https://&amp;lt;project&amp;gt;.supabase.co/auth/v1/callback&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blank page or "supabaseUrl is required":&lt;/strong&gt; with Vite, only &lt;code&gt;VITE_&lt;/code&gt;-prefixed vars reach the app, and they're baked in at build time. Add them on your host, then redeploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've checked those and it's still stuck, App Rescue is $149 for one issue. Send us your app and what's broken: &lt;a href="https://patchandship.com/app-rescue?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=app-rescue" rel="noopener noreferrer"&gt;App Rescue&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Moving off Lovable Cloud to your own Supabase
&lt;/h2&gt;

&lt;p&gt;Lovable can export your database, including your users' accounts. Files, secrets, sign-in settings and edge functions don't come with it, so those get set up again. You run the restore with your own database password; nobody helping you should need it. Don't remove Lovable Cloud until you've checked everything, since removing it deletes its database, backups, users and files.&lt;/p&gt;

&lt;p&gt;We do the move at a fixed price by size ($149, $349 or $599). What we move depends on the size: auth users on every size, storage files on Medium and Large, and edge functions on Large. Tell us about your app: &lt;a href="https://patchandship.com/lovable-cloud-to-supabase?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=cloud-move" rel="noopener noreferrer"&gt;Lovable Cloud to Supabase&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not affiliated with Lovable, Bolt, Replit, Cursor or Supabase.&lt;/p&gt;

</description>
      <category>lovable</category>
      <category>supabase</category>
      <category>webdev</category>
      <category>vite</category>
    </item>
    <item>
      <title>Lovable or Bolt app won't deploy: common fixes</title>
      <dc:creator>PatchAndShip</dc:creator>
      <pubDate>Fri, 30 Oct 2026 20:04:45 +0000</pubDate>
      <link>https://dev.to/patchandship/lovable-or-bolt-app-wont-deploy-common-fixes-2j1e</link>
      <guid>https://dev.to/patchandship/lovable-or-bolt-app-wont-deploy-common-fixes-2j1e</guid>
      <description>&lt;p&gt;It works in the editor. You hit publish, or push to Vercel or Netlify, and something breaks. The build fails, the page is blank, login bounces you back, or every page except the home page shows a 404.&lt;/p&gt;

&lt;p&gt;Most of these come down to a short list of causes. Here they are, in the order we'd check them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The build itself fails
&lt;/h2&gt;

&lt;p&gt;The preview in Lovable or Bolt is more forgiving than a production build. A production build runs &lt;code&gt;npm run build&lt;/code&gt;, and it stops on things the preview lets slide, like a type error or a missing import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the error. In Lovable, a failed publish shows a "Publishing failed" message or "Build errors need fixing", with a &lt;strong&gt;Try to fix&lt;/strong&gt; button. Use it, or describe the error in chat. (&lt;a href="https://docs.lovable.dev/features/publish" rel="noopener noreferrer"&gt;Lovable: Publish&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Fix the build errors (or let &lt;strong&gt;Try to fix&lt;/strong&gt; do it), then publish again. (&lt;a href="https://docs.lovable.dev/features/publish" rel="noopener noreferrer"&gt;Lovable: Publish&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;If Bolt can't publish to Netlify, Bolt's own fallback is: run the build command (usually &lt;code&gt;npm run build&lt;/code&gt;), download the project, and deploy it to Netlify by hand. (&lt;a href="https://support.bolt.new/troubleshooting/publish-issues" rel="noopener noreferrer"&gt;Bolt: Publishing issues&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Missing environment variables (the VITE_ prefix)
&lt;/h2&gt;

&lt;p&gt;Lovable and most Bolt apps are built with Vite. Vite only passes variables that start with &lt;code&gt;VITE_&lt;/code&gt; to your app in the browser. A variable called &lt;code&gt;SUPABASE_URL&lt;/code&gt; won't be there. (&lt;a href="https://vite.dev/guide/env-and-mode" rel="noopener noreferrer"&gt;Vite: Env variables&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Your host also doesn't read the &lt;code&gt;.env&lt;/code&gt; file from your project. Netlify says its build system doesn't read &lt;code&gt;.env&lt;/code&gt; files; you add the values in its settings instead. (&lt;a href="https://docs.netlify.com/build/configure-builds/environment-variables/" rel="noopener noreferrer"&gt;Netlify: Build environment variables&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copy the exact names from your project's &lt;code&gt;.env&lt;/code&gt; file into your host's environment variable settings. (&lt;a href="https://www.netlify.com/knowledge-base/deploy-your-lovable-app-to-netlify/" rel="noopener noreferrer"&gt;Netlify: Deploy your Lovable app&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Then rebuild. &lt;code&gt;VITE_&lt;/code&gt; values are baked in when the app builds, not when it runs. Adding them after the build changes nothing until you deploy again. (&lt;a href="https://docs.lovable.dev/tips-tricks/external-deployment-hosting" rel="noopener noreferrer"&gt;Lovable: Hosting outside Lovable&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Only put public values in &lt;code&gt;VITE_&lt;/code&gt; variables. Vite's own docs say they end up in the code anyone can download. Private keys belong in server functions. (&lt;a href="https://vite.dev/guide/env-and-mode" rel="noopener noreferrer"&gt;Vite: Env variables&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Supabase URL and key not set on the host
&lt;/h2&gt;

&lt;p&gt;This is the most common version of #2. The app loads, but nothing saves, lists are empty, or sign-in does nothing. Netlify's Lovable guide says a site deployed by drag-and-drop (no build, no environment variables) loads but fails every data call. (&lt;a href="https://www.netlify.com/knowledge-base/deploy-your-lovable-app-to-netlify/" rel="noopener noreferrer"&gt;Netlify: Deploy your Lovable app&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; set these on your host, copied from your &lt;code&gt;.env&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VITE_SUPABASE_URL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VITE_SUPABASE_PUBLISHABLE_KEY&lt;/code&gt; (older projects use &lt;code&gt;VITE_SUPABASE_ANON_KEY&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VITE_SUPABASE_PROJECT_ID&lt;/code&gt; if your Lovable project has it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(&lt;a href="https://docs.lovable.dev/tips-tricks/external-deployment-hosting" rel="noopener noreferrer"&gt;Lovable: Hosting outside Lovable&lt;/a&gt;, &lt;a href="https://www.netlify.com/knowledge-base/deploy-your-lovable-app-to-netlify/" rel="noopener noreferrer"&gt;Netlify: Deploy your Lovable app&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Newer Lovable projects that use server functions also need &lt;code&gt;SUPABASE_URL&lt;/code&gt; and &lt;code&gt;SUPABASE_PUBLISHABLE_KEY&lt;/code&gt; for the server side, plus any secrets those functions read. (&lt;a href="https://docs.lovable.dev/tips-tricks/external-deployment-hosting" rel="noopener noreferrer"&gt;Lovable: Hosting outside Lovable&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  4. 404 when you refresh a page
&lt;/h2&gt;

&lt;p&gt;The home page works. You click to &lt;code&gt;/dashboard&lt;/code&gt;, it works. You refresh, and get a 404. This happens because the host looks for a real file called &lt;code&gt;/dashboard&lt;/code&gt; and there isn't one. Your app handles that route in the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; tell the host to send every route to &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Netlify:&lt;/strong&gt; add a file &lt;code&gt;public/_redirects&lt;/code&gt; with this one line: &lt;code&gt;/* /index.html 200&lt;/code&gt;. (&lt;a href="https://docs.netlify.com/manage/routing/redirects/rewrites-proxies/" rel="noopener noreferrer"&gt;Netlify: Rewrites and proxies&lt;/a&gt;, &lt;a href="https://www.netlify.com/knowledge-base/deploy-your-bolt-app-to-netlify/" rel="noopener noreferrer"&gt;Netlify: Deploy your Bolt app&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel:&lt;/strong&gt; add a &lt;code&gt;vercel.json&lt;/code&gt; with a rewrite from &lt;code&gt;/(.*)&lt;/code&gt; to &lt;code&gt;/index.html&lt;/code&gt;. (&lt;a href="https://vercel.com/docs/project-configuration/vercel-json" rel="noopener noreferrer"&gt;Vercel: vercel.json&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One exception: Lovable projects created from May 13, 2026 are built on TanStack Start and run a server part. For those, a 404 usually means the host isn't running that server part, not a missing rewrite. Vercel and Netlify detect it on their own; on Vercel, it needs &lt;code&gt;@lovable.dev/vite-tanstack-config&lt;/code&gt; at &lt;code&gt;^2.6.2&lt;/code&gt; or higher. (&lt;a href="https://docs.lovable.dev/tips-tricks/external-deployment-hosting" rel="noopener noreferrer"&gt;Lovable: Hosting outside Lovable&lt;/a&gt;, &lt;a href="https://vercel.com/docs/integrations/lovable" rel="noopener noreferrer"&gt;Vercel: Lovable&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Login redirects back to the wrong place
&lt;/h2&gt;

&lt;p&gt;Sign-in works in the preview, but on your real domain users land on &lt;code&gt;localhost&lt;/code&gt;, the old preview link, or an error. Supabase only sends people to addresses you've allowed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; in your Supabase dashboard, open &lt;strong&gt;Authentication â†’ URL Configuration&lt;/strong&gt; (&lt;a href="https://supabase.com/dashboard/project/_/auth/url-configuration" rel="noopener noreferrer"&gt;direct link&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;strong&gt;Site URL&lt;/strong&gt; to your live address. Supabase says this setting is critical for email confirmations and password resets. (&lt;a href="https://supabase.com/docs/guides/auth/redirect-urls" rel="noopener noreferrer"&gt;Supabase: Redirect URLs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Add your live address, and any preview addresses you use, to &lt;strong&gt;Redirect URLs&lt;/strong&gt;. (&lt;a href="https://supabase.com/docs/guides/auth/redirect-urls" rel="noopener noreferrer"&gt;Supabase: Redirect URLs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;If you use Google sign-in or another OAuth provider, add your live domain to that provider's allowed redirect URLs too. (&lt;a href="https://docs.lovable.dev/tips-tricks/external-deployment-hosting" rel="noopener noreferrer"&gt;Lovable: Hosting outside Lovable&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Wrong Node version
&lt;/h2&gt;

&lt;p&gt;Bolt builds inside a browser-based environment that can differ from your host's. A build that passes in Bolt can fail on the host because of the Node version. (&lt;a href="https://www.netlify.com/knowledge-base/deploy-your-bolt-app-to-netlify/" rel="noopener noreferrer"&gt;Netlify: Deploy your Bolt app&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lovable's docs say to build with Node.js 22. (&lt;a href="https://docs.lovable.dev/tips-tricks/external-deployment-hosting" rel="noopener noreferrer"&gt;Lovable: Hosting outside Lovable&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;On Netlify, set it with a &lt;code&gt;.nvmrc&lt;/code&gt; file or a &lt;code&gt;NODE_VERSION&lt;/code&gt; variable. (&lt;a href="https://docs.netlify.com/build/configure-builds/environment-variables/" rel="noopener noreferrer"&gt;Netlify: Build environment variables&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;On Vercel, set it under &lt;strong&gt;Project Settings â†’ Build and Deployment â†’ Node.js Version&lt;/strong&gt;. (&lt;a href="https://vercel.com/docs/builds/configure-a-build" rel="noopener noreferrer"&gt;Vercel: Configuring a build&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Wrong build output folder (blank page)
&lt;/h2&gt;

&lt;p&gt;If the deploy "succeeds" but you see a blank page or a folder listing, the host may be serving the wrong folder. Vite writes the finished site to &lt;code&gt;dist&lt;/code&gt;. Create React App uses &lt;code&gt;build&lt;/code&gt;. (&lt;a href="https://www.netlify.com/knowledge-base/deploy-your-bolt-app-to-netlify/" rel="noopener noreferrer"&gt;Netlify: Deploy your Bolt app&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; set the build command to &lt;code&gt;npm run build&lt;/code&gt; and the publish (output) directory to &lt;code&gt;dist&lt;/code&gt;. On Vercel, Lovable says no output directory setting is needed. (&lt;a href="https://docs.lovable.dev/tips-tricks/external-deployment-hosting" rel="noopener noreferrer"&gt;Lovable: Hosting outside Lovable&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick check after any deploy
&lt;/h2&gt;

&lt;p&gt;Lovable suggests testing a new host before you switch over: open a deep page directly and refresh, sign in and out, and try the reads, writes and uploads your app depends on. (&lt;a href="https://docs.lovable.dev/tips-tricks/external-deployment-hosting" rel="noopener noreferrer"&gt;Lovable: Hosting outside Lovable&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why does my Lovable app work in the editor but not when deployed?&lt;/strong&gt;&lt;br&gt;
The production build is stricter than the preview, and your host doesn't have the values from your &lt;code&gt;.env&lt;/code&gt; file until you add them. Check the build error first, then your &lt;code&gt;VITE_&lt;/code&gt; variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do I get a 404 when I refresh a page?&lt;/strong&gt;&lt;br&gt;
The host is looking for a file that doesn't exist. Add a rewrite that sends every route to &lt;code&gt;index.html&lt;/code&gt;: &lt;code&gt;/* /index.html 200&lt;/code&gt; in Netlify's &lt;code&gt;_redirects&lt;/code&gt;, or a rewrite in &lt;code&gt;vercel.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does login send users to localhost?&lt;/strong&gt;&lt;br&gt;
Your Supabase Site URL still points at your old or local address. Change it to your live domain under Authentication â†’ URL Configuration, and add the domain to Redirect URLs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to rebuild after adding environment variables?&lt;/strong&gt;&lt;br&gt;
Yes. &lt;code&gt;VITE_&lt;/code&gt; values are baked in at build time, so deploy again after you add or change them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still stuck?
&lt;/h2&gt;

&lt;p&gt;Want to know if your live app is exposing keys? Our &lt;a href="https://patchandship.com/check.html?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=wont-deploy" rel="noopener noreferrer"&gt;free instant check&lt;/a&gt; scans your homepage and its JavaScript for leaked keys and reviews your security headers. It can't see your database.&lt;/p&gt;

&lt;p&gt;Want it fixed and deployed for you? Fix &amp;amp; Deploy ($249) is everything in the Security Fix plus the production deploy to one host (Vercel or Netlify, or Lovable or Replit hosting, depending on your builder): env vars, Supabase auth redirect URLs, custom domain with SSL, and a smoke test. &lt;a href="https://patchandship.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=wont-deploy#pricing" rel="noopener noreferrer"&gt;See Fix &amp;amp; Deploy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>lovable</category>
      <category>supabase</category>
      <category>vite</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A 12-step security checklist for vibe-coded apps on Supabase (2026)</title>
      <dc:creator>PatchAndShip</dc:creator>
      <pubDate>Fri, 30 Oct 2026 16:42:10 +0000</pubDate>
      <link>https://dev.to/patchandship/a-12-step-security-checklist-for-vibe-coded-apps-on-supabase-2026-2nl8</link>
      <guid>https://dev.to/patchandship/a-12-step-security-checklist-for-vibe-coded-apps-on-supabase-2026-2nl8</guid>
      <description>&lt;p&gt;You built an app with Lovable, Bolt, Replit, v0 or Cursor on top of Supabase, and it works in preview. Before real users show up, here's how to check whether their data is actually protected. For each step you get what to check, how to check it yourself, and what the fix looks like.&lt;/p&gt;

&lt;p&gt;One rule before we start: run every check against an app you own. Don't test someone else's app without permission, not even with a single &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; Row Level Security (RLS) on for every table in &lt;code&gt;public&lt;/code&gt;, every policy limited to the row's owner, no secret key in your frontend or repo, private files in private buckets, and Site URL pointing at production. Then test it all while logged out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scans found
&lt;/h2&gt;

&lt;p&gt;Two security vendors published scans this year. Both sell scanning tools, and they measured different things, so don't compare their numbers against each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sentris&lt;/strong&gt; (public code repositories, published Aug 5, 2026; vendor research): Sentris scanned 2,144 public repositories that use Supabase, not live apps. In Sentris's scan of public GitHub repositories, 57% of Lovable repos had a table in the public schema with Row Level Security switched off, compared with 21.7% of Supabase repos with no AI-tool marker. Sentris says this one check accounts for almost all of the gap. Leaked credentials showed up in 11.3% of Lovable repos and 11.2% of repos with no AI-tool marker, "the same rate, within noise". A public storage bucket turned up in 38.7% of Lovable repos, compared with 10.5% of repos with no AI-tool marker. (&lt;a href="https://sentris.dev/blog/we-scanned-2000-ai-built-supabase-apps" rel="noopener noreferrer"&gt;Sentris&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symbiotic Security&lt;/strong&gt; (live sites, published Jun 2, 2026; vendor research): In Symbiotic Security's scan of 1,072 vibe-coded sites that use Supabase, 172 let anyone delete database records without authentication, 172 allowed unauthenticated PATCH (modification), 39 had tables fully readable with the public Supabase key, and 14 allowed unauthenticated inserts. (&lt;a href="https://www.symbioticsec.ai/blog/we-scanned-1-072-vibe-coded-apps-98-had-security-flaws" rel="noopener noreferrer"&gt;Symbiotic Security&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Per Sentris, the gap between Lovable repos and repos with no AI-tool marker comes almost entirely from tables with RLS switched off, not from leaked credentials. So that's where we start.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Supabase security works, in one paragraph
&lt;/h2&gt;

&lt;p&gt;Your frontend talks to Supabase directly, using a key that ships in your JavaScript. Publishable (anon) keys are meant to be public. What actually protects your data is RLS together with grants: grants control whether a role can reach a table, RLS controls which rows it can access. The other key is a different story. Supabase: "A secret key bypasses every Row Level Security policy you have. Never put one in a browser, a shipped application, or source control." (&lt;a href="https://supabase.com/docs/guides/api/api-keys" rel="noopener noreferrer"&gt;API keys&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;p&gt;So "is my key exposed?" is usually the wrong question. The better questions are "which key?" and "what do my policies allow?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The 12 checks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Every table in &lt;code&gt;public&lt;/code&gt; has RLS enabled
&lt;/h3&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="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relkind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relrowsecurity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any table this returns has RLS off. The Security Advisor flags this as an error (lint 0013): "Anyone with your project URL can read, edit, and delete all data in this table because Row-Level Security is not enabled." (&lt;a href="https://supabase.com/docs/guides/database/database-advisors" rel="noopener noreferrer"&gt;Database Advisors&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;alter table public.your_table enable row level security;&lt;/code&gt; and add the policies from step 2 in the same migration. Supabase: "Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies." (&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Row Level Security&lt;/a&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Policies are per operation and scoped to the owner
&lt;/h3&gt;

&lt;p&gt;The Advisor flags always-true conditions such as &lt;code&gt;using (true)&lt;/code&gt; as a permissive policy (lint 0024, a warning). And when a request has no signed-in user, &lt;code&gt;auth.uid()&lt;/code&gt; returns null (&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Row Level Security&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Our guidance: &lt;code&gt;using (true)&lt;/code&gt; lets everyone in, and &lt;code&gt;auth.uid() is not null&lt;/code&gt; on its own lets any signed-in user reach every other user's rows. Compare &lt;code&gt;auth.uid()&lt;/code&gt; to the owner column:&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;"orders_select_own"&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;orders&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="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;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"orders_insert_own"&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;orders&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="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;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"orders_update_own"&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;orders&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&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="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;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="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;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"orders_delete_own"&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;orders&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;delete&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="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;For updates, &lt;code&gt;using&lt;/code&gt; decides which existing rows can be updated and &lt;code&gt;with check&lt;/code&gt; decides what the resulting row may look like, which stops a user from reassigning &lt;code&gt;user_id&lt;/code&gt;. If &lt;code&gt;with check&lt;/code&gt; is omitted, &lt;code&gt;using&lt;/code&gt; applies to both (&lt;a href="https://www.postgresql.org/docs/current/sql-createpolicy.html" rel="noopener noreferrer"&gt;PostgreSQL: CREATE POLICY&lt;/a&gt;). We write both so the rule is explicit. Wrapping it as &lt;code&gt;(select auth.uid())&lt;/code&gt; lets Postgres cache the result per statement via an initPlan, which improves RLS performance (&lt;a href="https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv" rel="noopener noreferrer"&gt;RLS performance and best practices&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; sign in as user A, grab the ID of a record owned by user B, and try to load, edit and delete it. Every attempt should fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Views don't quietly bypass RLS
&lt;/h3&gt;

&lt;p&gt;The Advisor flags a security definer view as an error (lint 0010). Views bypass RLS by default because they run with the owner's permissions, usually &lt;code&gt;postgres&lt;/code&gt; in Supabase. On Postgres 15+:&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="k"&gt;view&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;public_profiles&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;security_invoker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&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;display_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;avatar_url&lt;/span&gt; &lt;span class="k"&gt;from&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;profiles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On older versions, revoke access from &lt;code&gt;anon&lt;/code&gt; and &lt;code&gt;authenticated&lt;/code&gt;, or move the view to an unexposed schema (&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Row Level Security&lt;/a&gt;, &lt;a href="https://www.postgresql.org/docs/current/sql-createview.html" rel="noopener noreferrer"&gt;PostgreSQL: CREATE VIEW&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The logged-out test returns only public data
&lt;/h3&gt;

&lt;p&gt;In your live app, open DevTools, then Network, find a request to &lt;code&gt;https://&amp;lt;project&amp;gt;.supabase.co/rest/v1/...&lt;/code&gt; and copy the &lt;code&gt;apikey&lt;/code&gt; header. Then:&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://&amp;lt;project&amp;gt;.supabase.co/rest/v1/&amp;lt;table&amp;gt;?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: &amp;lt;public-key&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Private tables should return &lt;code&gt;[]&lt;/code&gt; or a permission error. If one returns rows, anyone can do what you just did. Own apps only.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. No secret key in the frontend or the repo history
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Search the &lt;strong&gt;built&lt;/strong&gt; bundle: &lt;code&gt;npm run build&lt;/code&gt;, then &lt;code&gt;grep -rE "sk_live_|sb_secret_|service_role|c2VydmljZV9yb2xl|NlcnZpY2Vfcm9s|zZXJ2aWNlX3Jv" dist/&lt;/code&gt;. The last three patterns catch a legacy &lt;code&gt;service_role&lt;/code&gt; key: it's a JWT (starts with &lt;code&gt;eyJ&lt;/code&gt;), so the word "service_role" only appears base64-encoded inside it.&lt;/li&gt;
&lt;li&gt;Vite: "Variables prefixed with &lt;code&gt;VITE_&lt;/code&gt; will be exposed in client-side source code after Vite bundling." (&lt;a href="https://vite.dev/guide/env-and-mode" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;History: &lt;code&gt;git log -p --all | grep -E "sk_live_|sb_secret_|service_role|c2VydmljZV9yb2xl|NlcnZpY2Vfcm9s|zZXJ2aWNlX3Jv"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a secret was committed, GitHub says to revoke or rotate it first. Removing it means rewriting history, and old commits may still be reachable in clones, forks, cached views and pull requests (&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository" rel="noopener noreferrer"&gt;GitHub Docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix order (our guidance):&lt;/strong&gt; move the code to a server (Edge Function or API route), create a new key, deploy it everywhere, then revoke the old one. Revoke last, or you'll break production mid-deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Storage buckets match what's in them
&lt;/h3&gt;

&lt;p&gt;Buckets "are private by default". For a public bucket, "anyone who possesses the asset URL can readily access the file" (&lt;a href="https://supabase.com/docs/guides/storage/buckets/fundamentals" rel="noopener noreferrer"&gt;Storage Buckets&lt;/a&gt;). Check which buckets are Public, and whether any of them hold per-user files like IDs, invoices or exports. Make those private and add policies on &lt;code&gt;storage.objects&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. New public tables need explicit grants. Never GRANT ALL to anon
&lt;/h3&gt;

&lt;p&gt;From Oct 30, 2026, once the change reaches your project, new tables in &lt;code&gt;public&lt;/code&gt; need an explicit grant before the Data API can reach them; existing tables keep their current grants. New projects have had this by default since the May 30, 2026 rollout (&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;). The risky quick fix is &lt;code&gt;grant all ... to anon&lt;/code&gt;: once a grant exists, RLS is the only guard.&lt;/p&gt;

&lt;p&gt;More on this: &lt;a href="https://dev.to/patchandship/grant-all-to-anon-is-the-wrong-fix-for-supabase-42501-after-oct-30-1bdm"&gt;GRANT ALL to anon is the wrong fix for Supabase 42501 after Oct 30&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our rule: grant only what each role needs, keep RLS on with policies, and never GRANT ALL to anon. Put the grant, RLS and policies in one migration:&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;begin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;orders&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;revoke&lt;/span&gt; &lt;span class="k"&gt;all&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;orders&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&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;orders&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;-- plus the four policies from step 2&lt;/span&gt;
&lt;span class="k"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. Auth URLs point at production
&lt;/h3&gt;

&lt;p&gt;Supabase: change the Site URL "from &lt;code&gt;http://localhost:3000&lt;/code&gt; to your production URL"; it's "critical for email confirmations and password resets". Use exact redirect URLs in production, not wildcards (&lt;a href="https://supabase.com/docs/guides/auth/redirect-urls" rel="noopener noreferrer"&gt;Redirect URLs&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Payment state comes from verified webhooks
&lt;/h3&gt;

&lt;p&gt;If the browser can mark a user "paid", so can anyone else. Set the flag only in a server-side handler for a verified Stripe webhook, and verify the &lt;code&gt;Stripe-Signature&lt;/code&gt; header first. Stripe says endpoints "might occasionally receive the same event more than once", so log processed event IDs and skip repeats (&lt;a href="https://docs.stripe.com/webhooks" rel="noopener noreferrer"&gt;Stripe: Webhooks&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Edge Functions check who's calling
&lt;/h3&gt;

&lt;p&gt;OWASP Top 10:2025 keeps Broken Access Control at #1 (A01), including "an accessible API with missing access controls for POST, PUT, and DELETE" (&lt;a href="https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/" rel="noopener noreferrer"&gt;OWASP&lt;/a&gt;). For each function, ask what stops a stranger from calling it. The answer should be a verified user JWT (plus a per-record check), a verified webhook signature, or a shared secret for cron. "Nobody knows the URL" doesn't count. Supabase's default is &lt;code&gt;verify_jwt = true&lt;/code&gt;; leave it on for user-facing functions. For Stripe-style webhooks, keep &lt;code&gt;verify_jwt = false&lt;/code&gt; and verify the signature in the handler (&lt;a href="https://supabase.com/docs/guides/functions/auth" rel="noopener noreferrer"&gt;Functions auth&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  11. Rate limits on anything that costs money or sends mail
&lt;/h3&gt;

&lt;p&gt;Our guidance: endpoints that call a paid AI API, send email or SMS, or create accounts can be run up by a script. Add CAPTCHA or bot protection to sign-up if you see abuse, cap per-user usage on the server, and set spending limits with your providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  12. You know what's deployed
&lt;/h3&gt;

&lt;p&gt;Preview, repo and production drift apart over time. After any fix, re-run the logged-out test (step 4) and the two-account test (step 2) against production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Got 15 minutes?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run the RLS query in step 1.&lt;/li&gt;
&lt;li&gt;Run the logged-out &lt;code&gt;curl&lt;/code&gt; against your three most sensitive tables.&lt;/li&gt;
&lt;li&gt;Run the two greps in step 5 (they cover &lt;code&gt;sb_secret_&lt;/code&gt;, &lt;code&gt;sk_live_&lt;/code&gt; and legacy &lt;code&gt;service_role&lt;/code&gt; JWTs).&lt;/li&gt;
&lt;li&gt;Check which buckets are public.&lt;/li&gt;
&lt;li&gt;Add one line to your AI tool's project instructions: &lt;em&gt;Every new table must enable RLS, grant only needed operations, and have one owner-scoped policy per operation, in the same migration.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a penetration test, a compliance audit or a guarantee. Apps that handle health, payment-card or children's data need more than this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sentris.dev/blog/we-scanned-2000-ai-built-supabase-apps" rel="noopener noreferrer"&gt;Sentris: We scanned 2,144 AI-built Supabase repos&lt;/a&gt; (Aug 5, 2026; vendor research)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.symbioticsec.ai/blog/we-scanned-1-072-vibe-coded-apps-98-had-security-flaws" rel="noopener noreferrer"&gt;Symbiotic Security: We Scanned 1,072 Vibe Coded Apps&lt;/a&gt; (Jun 2, 2026; vendor research)&lt;/li&gt;
&lt;li&gt;Supabase Docs: &lt;a href="https://supabase.com/docs/guides/database/database-advisors" rel="noopener noreferrer"&gt;Database Advisors&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;a href="https://supabase.com/docs/guides/api/api-keys" rel="noopener noreferrer"&gt;API keys&lt;/a&gt;, &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Row Level Security&lt;/a&gt;, &lt;a href="https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv" rel="noopener noreferrer"&gt;RLS performance&lt;/a&gt;, &lt;a href="https://supabase.com/docs/guides/storage/buckets/fundamentals" rel="noopener noreferrer"&gt;Storage Buckets&lt;/a&gt;, &lt;a href="https://supabase.com/docs/guides/auth/redirect-urls" rel="noopener noreferrer"&gt;Redirect URLs&lt;/a&gt;, &lt;a href="https://supabase.com/docs/guides/functions/auth" rel="noopener noreferrer"&gt;Functions auth&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&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: Tables not exposed to Data and GraphQL API automatically&lt;/a&gt; (Apr 28, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vite.dev/guide/env-and-mode" rel="noopener noreferrer"&gt;Vite: Env Variables and Modes&lt;/a&gt;, &lt;a href="https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/" rel="noopener noreferrer"&gt;OWASP A01:2025&lt;/a&gt;, &lt;a href="https://docs.stripe.com/webhooks" rel="noopener noreferrer"&gt;Stripe: Webhooks&lt;/a&gt;, &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository" rel="noopener noreferrer"&gt;GitHub: Removing sensitive data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.postgresql.org/docs/current/sql-createpolicy.html" rel="noopener noreferrer"&gt;PostgreSQL: CREATE POLICY&lt;/a&gt;, &lt;a href="https://www.postgresql.org/docs/current/sql-createview.html" rel="noopener noreferrer"&gt;CREATE VIEW&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Checked Oct 27, 2026. Lines marked "our guidance" are ours, not the sources'.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The full checklist, with the CVE background and more detail on each step, is &lt;a href="https://patchandship.com/blog/vibe-coded-app-security-checklist?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=devto-checklist" rel="noopener noreferrer"&gt;on our site&lt;/a&gt;. If you'd like a second look, there's a &lt;a href="https://patchandship.com/check.html?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=devto-checklist" rel="noopener noreferrer"&gt;free check&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not affiliated with Supabase.&lt;/p&gt;

&lt;p&gt;PatchAndShip Team&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>The easiest planted hole to misread in our Supabase demo</title>
      <dc:creator>PatchAndShip</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:20:13 +0000</pubDate>
      <link>https://dev.to/patchandship/the-easiest-planted-hole-to-misread-in-our-supabase-demo-5ddh</link>
      <guid>https://dev.to/patchandship/the-easiest-planted-hole-to-misread-in-our-supabase-demo-5ddh</guid>
      <description>&lt;p&gt;We planted a few holes in a throwaway demo Supabase project to see how each one looks from the outside. The easiest one to misread was the secret key in the frontend. A long eyJ... string in your bundle looks alarming, but the public anon key looks exactly like that too. What matters is whether one decodes to "role":"service_role", or whether a newer key starts with sb_secret_ (sb_publishable_ is meant to be public).&lt;/p&gt;

&lt;p&gt;How do you all check what your app actually ships to the browser?&lt;/p&gt;

&lt;p&gt;For our launch, we're doing a free 15-minute look at Supabase database rules for the first 10 apps. Read-only, no keys needed. Comment here or email &lt;a href="mailto:patchandship@gmail.com"&gt;patchandship@gmail.com&lt;/a&gt; if you want one.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>GRANT ALL to anon is the wrong fix for Supabase 42501 after Oct 30</title>
      <dc:creator>PatchAndShip</dc:creator>
      <pubDate>Tue, 27 Oct 2026 16:48:51 +0000</pubDate>
      <link>https://dev.to/patchandship/grant-all-to-anon-is-the-wrong-fix-for-supabase-42501-after-oct-30-1bdm</link>
      <guid>https://dev.to/patchandship/grant-all-to-anon-is-the-wrong-fix-for-supabase-42501-after-oct-30-1bdm</guid>
      <description>&lt;p&gt;From Oct 30, 2026, Supabase will apply a change to existing projects: new tables in &lt;code&gt;public&lt;/code&gt; won't be visible to the Data API until you grant access. Supabase's changelog puts it like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Once the change is rolled out to your project, new tables you create in &lt;code&gt;public&lt;/code&gt; schema require an explicit opt-in (via a Postgres &lt;code&gt;grant&lt;/code&gt;) before the Data API can see them. Existing tables are not affected in your project, they keep their current grants and stay reachable."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the change reaches your project, the first new table you add will likely fail with &lt;code&gt;42501 permission denied for table&lt;/code&gt;. And the quickest-looking fix, often the one an AI assistant suggests, is this:&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="c1"&gt;-- Don't run this&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want the full walkthrough of what 42501 means, we covered it in &lt;a href="https://dev.to/patchandship/supabase-error-42501-permission-denied-for-table-the-safe-fix-before-oct-30-1k23"&gt;the first article&lt;/a&gt;. This one is about the fix: keep RLS on, grant only what each table needs, and check your older tables too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the error before you fix it
&lt;/h2&gt;

&lt;p&gt;Three results look alike but mean different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;42501 permission denied for table&lt;/code&gt;&lt;/strong&gt;: the role has no grant. You need a specific grant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An empty list, no error&lt;/strong&gt;: the grant is there, and RLS is hiding rows. Check your policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;42501 new row violates row-level security policy&lt;/code&gt;&lt;/strong&gt;: RLS is working and blocked the write. Fix the policy or the data you send, not the grants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the first one is a grant problem. A broad grant does nothing for the other two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why GRANT ALL to anon is wrong
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;anon&lt;/code&gt; is the role behind your public anon key, and that key ships in every browser that loads your app. Any signed-out request runs as &lt;code&gt;anon&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GRANT ALL&lt;/code&gt; gives that role every table privilege Postgres has: &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;code&gt;TRUNCATE&lt;/code&gt;, &lt;code&gt;REFERENCES&lt;/code&gt;, &lt;code&gt;TRIGGER&lt;/code&gt;, and, on Postgres 17 and later, &lt;code&gt;MAINTAIN&lt;/code&gt;. A signed-out visitor has no use for most of those.&lt;/p&gt;

&lt;p&gt;After that, RLS is the only thing deciding what happens. If RLS is off on a table, or a policy is permissive (like &lt;code&gt;using (true)&lt;/code&gt;), anyone with your public key can read or write that table. The grant removed one layer of protection and left everything to the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: turn RLS on first
&lt;/h2&gt;

&lt;p&gt;Enable RLS before you grant anything, in the same migration:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With RLS on and no policies, the table returns nothing to API roles. That's a safe starting point. Then add the grants and policies the app actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: grant only the verbs each table uses
&lt;/h2&gt;

&lt;p&gt;For a typical per-user table, signed-in users work with their own rows and signed-out visitors need nothing:&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="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;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;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users read 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="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;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users 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="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;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users update 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;update&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="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;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="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;Add &lt;code&gt;delete&lt;/code&gt; (and a matching delete policy) only if the app deletes rows. No grant to &lt;code&gt;anon&lt;/code&gt; at all: a signed-out request gets a 42501, which is correct here.&lt;/p&gt;

&lt;p&gt;For a table that really is public, like a published product list, &lt;code&gt;anon&lt;/code&gt; gets &lt;code&gt;select&lt;/code&gt; and nothing else, with a policy that limits which rows:&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;products&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="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;products&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;anon&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;"Anyone reads published products"&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;products&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;anon&lt;/span&gt;&lt;span class="p"&gt;,&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;published&lt;/span&gt; &lt;span class="o"&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;p&gt;If the table has a &lt;code&gt;serial&lt;/code&gt; id, the role that inserts also needs the sequence:&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;pg_get_serial_sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public.your_table'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- usually returns public.your_table_id_seq&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;usage&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;sequence&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_id_seq&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;h2&gt;
  
  
  Step 3: check your older tables too
&lt;/h2&gt;

&lt;p&gt;The changelog says existing tables "keep their current grants and stay reachable". Tables created under the old defaults may give &lt;code&gt;anon&lt;/code&gt; more than a new one would, so review them too.&lt;/p&gt;

&lt;p&gt;These two queries only read metadata. First, what &lt;code&gt;anon&lt;/code&gt; and &lt;code&gt;authenticated&lt;/code&gt; can do on each table:&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="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&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;string_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;privilege_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;privilege_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;privileges&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="n"&gt;grantee&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'authenticated'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, which tables have RLS off:&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;tablename&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;schemaname&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="n"&gt;rowsecurity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with any table that's in the second list and also shows grants for &lt;code&gt;anon&lt;/code&gt; or &lt;code&gt;authenticated&lt;/code&gt; in the first. Before you revoke anything, check what your signed-out pages actually read, so you don't break a public page. To take back an &lt;code&gt;anon&lt;/code&gt; grant and keep only reads:&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;revoke&lt;/span&gt; &lt;span class="k"&gt;all&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;from&lt;/span&gt; &lt;span class="n"&gt;anon&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="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;to&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- only if the table is truly public&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick check
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Which 42501 is it: "permission denied" or "violates row-level security policy"?&lt;/li&gt;
&lt;li&gt;RLS enabled on the table before any grant?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;anon&lt;/code&gt; has no grant, or only &lt;code&gt;select&lt;/code&gt; on a truly public table?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;authenticated&lt;/code&gt; has only the verbs the app uses, each with an owner policy?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;serial&lt;/code&gt; id? Sequence &lt;code&gt;usage&lt;/code&gt; granted to the inserting role?&lt;/li&gt;
&lt;li&gt;Older tables reviewed with the two queries above?&lt;/li&gt;
&lt;li&gt;Tested as a real signed-in and signed-out user, not in the SQL editor?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters. The SQL editor runs as &lt;code&gt;postgres&lt;/code&gt;, which bypasses RLS, so a query that works there tells you nothing about what your users see. Test from the app, or use the dashboard's &lt;a href="https://supabase.com/features/user-impersonation" rel="noopener noreferrer"&gt;user impersonation&lt;/a&gt; to run queries as a specific role or user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;When Oct 30 reaches your project and a new table throws 42501, the fix is a few lines, not one big grant: RLS on first, the specific verbs each role needs, and a review of the tables you already have.&lt;/p&gt;

&lt;p&gt;Not affiliated with Supabase.&lt;/p&gt;

&lt;p&gt;Want a quick outside look at what your app's public files expose? &lt;a href="https://patchandship.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=oct30-countdown&amp;amp;utm_content=devto-02#free-check" rel="noopener noreferrer"&gt;Run the free check&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Supabase error 42501 "permission denied for table": the safe fix before Oct 30</title>
      <dc:creator>PatchAndShip</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:50:05 +0000</pubDate>
      <link>https://dev.to/patchandship/supabase-error-42501-permission-denied-for-table-the-safe-fix-before-oct-30-1k23</link>
      <guid>https://dev.to/patchandship/supabase-error-42501-permission-denied-for-table-the-safe-fix-before-oct-30-1k23</guid>
      <description>&lt;p&gt;If a request to your Supabase table suddenly fails with &lt;code&gt;42501 "permission denied for table"&lt;/code&gt;, the role making the request has no grant on that table. It's a one-line error with a tempting one-line fix, and that fix is exactly what you want to avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Grant only what each role needs, keep RLS on with policies, and never &lt;code&gt;GRANT ALL&lt;/code&gt; to anon.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Status as of Oct 31, 2026: Supabase said it would start applying this change to existing projects from Oct 30. Once it reaches a project, new tables in public need a grant before the Data API can see them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Supabase will change
&lt;/h2&gt;

&lt;p&gt;Here's how Supabase's changelog puts it: once the change reaches a project, "new tables you create in &lt;code&gt;public&lt;/code&gt; schema require an explicit opt-in (via a Postgres &lt;code&gt;grant&lt;/code&gt;) before the Data API can see them." The Data API is the auto-generated REST and GraphQL layer, the thing &lt;code&gt;supabase-js&lt;/code&gt; talks to.&lt;/p&gt;

&lt;p&gt;The timeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;April 28, 2026:&lt;/strong&gt; new projects could opt in at creation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;May 30, 2026:&lt;/strong&gt; became the default for all new projects, rolled out gradually over a few weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;October 30, 2026:&lt;/strong&gt; Supabase said it would start applying it to existing projects from Oct 30.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What stays the same, per Supabase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing tables keep their current grants and stay reachable.&lt;/li&gt;
&lt;li&gt;RLS behavior is unchanged.&lt;/li&gt;
&lt;li&gt;Apps that only use a direct Postgres connection aren't affected.&lt;/li&gt;
&lt;li&gt;Only the &lt;code&gt;public&lt;/code&gt; schema is affected. &lt;code&gt;storage&lt;/code&gt;, &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;realtime&lt;/code&gt; and custom schemas aren't.&lt;/li&gt;
&lt;/ul&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: Tables not exposed to Data and GraphQL API automatically&lt;/a&gt; (published Apr 28, 2026).&lt;/p&gt;

&lt;h2&gt;
  
  
  What the error looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&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="err"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response also includes a hint with a suggested &lt;code&gt;GRANT&lt;/code&gt; statement. Read it before you paste it. The right grant depends on who should see the data.&lt;/p&gt;

&lt;p&gt;Same code, different cause: if the message says "new row violates row-level security policy", your grant is fine. An RLS policy's &lt;code&gt;with check&lt;/code&gt; rejected the row. Fix the policy, leave the grants alone, and don't turn RLS off. (&lt;a href="https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors" rel="noopener noreferrer"&gt;Supabase: Database API 42501 errors&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;GRANT ALL&lt;/code&gt; to anon is the wrong fix
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;anon&lt;/code&gt; is the role behind your app's public key, the one that ships in your JavaScript. Your AI tool may suggest &lt;code&gt;grant all on table ... to anon&lt;/code&gt;. The error disappears. Here's what you gave up to make that happen.&lt;/p&gt;

&lt;p&gt;Supabase spells out the order of checks: "If the grant is missing, Postgres rejects the query before RLS comes into play." (&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;) Grants decide whether a role can reach a table. RLS decides which rows it can see (&lt;a href="https://supabase.com/docs/guides/api/securing-your-api" rel="noopener noreferrer"&gt;Supabase: Securing your API&lt;/a&gt;). Once the grant is in place, RLS is the only thing between a visitor and your data.&lt;/p&gt;

&lt;p&gt;And if RLS is off on that table, Supabase's Security Advisor (lint 0013) doesn't sugarcoat it: "Anyone with your project URL can read, edit, and delete all data in this table because Row-Level Security is not enabled." (&lt;a href="https://supabase.com/docs/guides/database/database-advisors" rel="noopener noreferrer"&gt;Supabase: Database Advisors&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Supabase's advice: "Use both controls for every exposed object."&lt;/p&gt;

&lt;h2&gt;
  
  
  The safe fix: grant, RLS and policies in one migration
&lt;/h2&gt;

&lt;p&gt;Supabase's recommended fix treats three things as one unit: the grant, enabling RLS, and the policies you need. Ship them in one migration so none of them can go out without the others.&lt;/p&gt;

&lt;p&gt;Here's an example for an &lt;code&gt;orders&lt;/code&gt; table where each row belongs to one signed-in user via &lt;code&gt;user_id&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;begin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;orders&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;revoke&lt;/span&gt; &lt;span class="k"&gt;all&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;orders&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&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;orders&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;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"orders_select_own"&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;orders&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="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;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"orders_insert_own"&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;orders&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="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;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"orders_update_own"&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;orders&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&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="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;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="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;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"orders_delete_own"&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;orders&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;delete&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="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;span class="k"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supabase's changelog example also grants &lt;code&gt;service_role&lt;/code&gt;. Add that if your own server code reads or writes the table:&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;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;orders&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only grant &lt;code&gt;anon&lt;/code&gt; access to data that signed-out visitors really need, and usually only &lt;code&gt;select&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test it:&lt;/strong&gt; sign out and read the table with just the public key. You should get an error or an empty list. Only test projects you own or are authorized to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  New projects hit this too
&lt;/h2&gt;

&lt;p&gt;New projects have behaved this way by default since the May 30, 2026 rollout, so a brand-new project may need the same grants today.&lt;/p&gt;

&lt;h2&gt;
  
  
  If an AI tool writes your migrations
&lt;/h2&gt;

&lt;p&gt;Give it the rule before it writes the migration. Paste this into your project instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When you add a new table to Supabase, write one migration that does three things together: grant only the access each role needs (never GRANT ALL to anon), enable Row Level Security on the table, and add one policy per operation that limits each signed-in user to their own rows. Do not turn Row Level Security off to make an error go away.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check what it writes against the example above before you run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Will my existing tables break?&lt;/strong&gt; No. Supabase says existing tables keep their grants and stay reachable, and RLS behavior is unchanged. The change only covers new tables in &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it affect storage or auth tables?&lt;/strong&gt; No. Only default privileges in &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do I see affected tables?&lt;/strong&gt; In the Security Advisor. Supabase: "From today through October 30, the Security Advisor flags affected tables per project and shows the remediation SQL." Supabase also planned email notices to owners and admins of active projects on September 23 and October 23, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&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: Breaking Change: Tables not exposed to Data and GraphQL API automatically&lt;/a&gt; (Apr 28, 2026): timeline, scope, error 42501, recommended fix, Security Advisor.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://supabase.com/docs/guides/api/securing-your-api" rel="noopener noreferrer"&gt;Supabase Docs: Securing your API&lt;/a&gt;: grants and RLS.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://supabase.com/docs/guides/database/database-advisors" rel="noopener noreferrer"&gt;Supabase Docs: Database Advisors&lt;/a&gt;: lint 0013.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All checked Oct 26, 2026. The rule "grant only what each role needs…" is our own guidance.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The full guide, kept up to date after Oct 30, is &lt;a href="https://patchandship.com/blog/supabase-42501-permission-denied-oct-30" rel="noopener noreferrer"&gt;on our site&lt;/a&gt;. Want a second pair of eyes on your app? Try the &lt;a href="https://patchandship.com/check.html" rel="noopener noreferrer"&gt;free instant check&lt;/a&gt; (results in your browser in a few seconds, and it doesn't store your results) or ask for the &lt;a href="https://patchandship.com/mini-check.html" rel="noopener noreferrer"&gt;free emailed mini check&lt;/a&gt; (one page within 24 hours). For app owners or people they authorize.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;PatchAndShip Team&lt;/p&gt;

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