<?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: Ayondip Jana</title>
    <description>The latest articles on DEV Community by Ayondip Jana (@detronetdip).</description>
    <link>https://dev.to/detronetdip</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%2F942366%2F4f06ee93-22bf-4ac5-8bda-620dc03e7a5f.png</url>
      <title>DEV Community: Ayondip Jana</title>
      <link>https://dev.to/detronetdip</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/detronetdip"/>
    <language>en</language>
    <item>
      <title>Supabase RLS checklist for Lovable and Bolt apps: what we found in 38 codebases</title>
      <dc:creator>Ayondip Jana</dc:creator>
      <pubDate>Sun, 01 Nov 2026 06:24:19 +0000</pubDate>
      <link>https://dev.to/detronetdip/supabase-rls-checklist-for-lovable-and-bolt-apps-what-we-found-in-38-codebases-1a2g</link>
      <guid>https://dev.to/detronetdip/supabase-rls-checklist-for-lovable-and-bolt-apps-what-we-found-in-38-codebases-1a2g</guid>
      <description>&lt;p&gt;Picture an invoicing app called Billtrack, built in a weekend with Lovable. It has a Supabase database with one table, &lt;code&gt;invoices&lt;/code&gt;, and two users, Alice and Bob. Alice should see her two invoices. Bob should see his one.&lt;/p&gt;

&lt;p&gt;The app works. Alice logs in and sees her invoices. Then anyone who opens the browser's network tab can copy the Supabase URL and the anon key out of a request, run one &lt;code&gt;curl&lt;/code&gt;, and get all three invoices, including Bob's.&lt;/p&gt;

&lt;p&gt;That is the bug behind CVE-2025-48757. A security researcher scanned 1,645 apps from Lovable's public showcase and found 170 with Supabase tables readable using only the public key. The exposed data included emails, addresses and in some cases API keys. The NVD entry notes that Lovable disputes the CVE, on the grounds that each customer is responsible for protecting their own app's data. Either way, the fix is in the app's database.&lt;/p&gt;

&lt;p&gt;It wasn't a one-off. In October 2025, the security firm Escape.tech scanned more than 5,600 public apps built with Lovable, Base44, Create.xyz, Vibe Studio and Bolt.new. It found more than 2,000 vulnerabilities, more than 400 exposed secrets and 175 cases of exposed personal data, including medical records and bank account numbers. It named missing or misconfigured RLS and Supabase service role keys sitting in frontend bundles among the main causes.&lt;/p&gt;

&lt;p&gt;We wanted to know whether that is still true a year later, so we looked at the code ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found in 38 public Lovable codebases
&lt;/h2&gt;

&lt;p&gt;In October 2026 we searched GitHub for public repositories generated by Lovable, using the &lt;code&gt;lovable-tagger&lt;/code&gt; package it adds to every project. We found 933. Of those, 218 include their Supabase migrations, the SQL files that create the database tables and their security rules. We picked 40 at random, and 38 of them defined tables in the &lt;code&gt;public&lt;/code&gt; schema. We replayed each repository's migrations in order and recorded the final state of every table, then checked the serious findings by hand.&lt;/p&gt;

&lt;p&gt;We only read code. We didn't connect to any live app or database, and we don't name any repository here.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Repositories&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;At least one table with RLS turned off&lt;/td&gt;
&lt;td&gt;2 of 38&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policies that let anyone, signed in or not, update or delete every row&lt;/td&gt;
&lt;td&gt;6 of 38&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policies that let any signed-in user update or delete every row&lt;/td&gt;
&lt;td&gt;4 of 38&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At least one of the three above&lt;/td&gt;
&lt;td&gt;10 of 38&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A view that doesn't apply the table's RLS (&lt;code&gt;security_invoker&lt;/code&gt; not set)&lt;/td&gt;
&lt;td&gt;6 of 38&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row surprised us. Only 2 of the 826 tables we found had RLS off. Missing RLS, the problem behind CVE-2025-48757, has become rare in Lovable's generated code. The risk has moved into the policies themselves, which a check for "is RLS on" does not catch.&lt;/p&gt;

&lt;p&gt;Two patterns stood out.&lt;/p&gt;

&lt;p&gt;The first is a temporary policy that never left. One billing app created &lt;code&gt;FOR ALL USING (true)&lt;/code&gt; policies on its customers, invoices and payments tables, with a comment saying they allowed all operations "for now" and could be restricted later. Later migrations added proper per-user policies next to them but never dropped the open ones. Because Postgres combines policies with OR, the open policy still wins, so anyone with the public key could read and change every invoice.&lt;/p&gt;

&lt;p&gt;The second is a policy named for one role that applies to all of them. Another app had a policy called "Service can manage all jobs" with &lt;code&gt;FOR ALL USING (true)&lt;/code&gt; and no &lt;code&gt;TO service_role&lt;/code&gt; clause. Without that clause a policy applies to every role, so the background-job table was writable by anonymous visitors.&lt;/p&gt;

&lt;p&gt;Some of these repositories are prototypes, and a live database can differ from its migrations if someone changed it by hand. So these numbers describe what the code would create, not what is running. They are still the code a founder would deploy.&lt;/p&gt;

&lt;p&gt;The rest of this post shows exactly what each kind of mistake exposes, and ends with the checklist we use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI-built apps hit this so often
&lt;/h2&gt;

&lt;p&gt;A traditional web app has a backend server between the browser and the database. The server decides who can see what.&lt;/p&gt;

&lt;p&gt;Apps generated by Lovable, Bolt and similar tools usually skip that server. The browser talks to Supabase directly, through an auto-generated REST API, and it sends a key that is meant to be public: the anon key. Supabase's own docs describe what makes that safe. Postgres row level security (RLS) runs on every request and filters rows per user. The docs put it plainly: enable RLS on every table in an exposed schema.&lt;/p&gt;

&lt;p&gt;So the anon key is not the secret. The RLS policies are the security model. If a table has RLS off, the public key opens all of it. RLS is not switched on for you when a table is created with SQL. Supabase's docs point to event triggers if you want that to happen automatically, and they note that on some projects a new table in &lt;code&gt;public&lt;/code&gt; starts with every privilege already granted to all three API roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we tested
&lt;/h2&gt;

&lt;p&gt;We rebuilt Billtrack's database in a lab: PostgreSQL 16, PostgREST 12.2.3 (the API layer Supabase uses), the same three roles Supabase uses (&lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;authenticated&lt;/code&gt;, &lt;code&gt;service_role&lt;/code&gt;), and an &lt;code&gt;auth.uid()&lt;/code&gt; function that reads the user ID from the request's JWT, as Supabase does. Then we called the API as an anonymous visitor, as Alice, and as the service role, after each change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State of the &lt;code&gt;invoices&lt;/code&gt; table&lt;/th&gt;
&lt;th&gt;Anonymous visitor&lt;/th&gt;
&lt;th&gt;Alice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RLS off&lt;/td&gt;
&lt;td&gt;All 3 invoices&lt;/td&gt;
&lt;td&gt;All 3 invoices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS on, no policies&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS on, policy scoped to &lt;code&gt;auth.uid()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;Her 2 invoices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same, plus an extra &lt;code&gt;using (true)&lt;/code&gt; policy&lt;/td&gt;
&lt;td&gt;All 3 invoices&lt;/td&gt;
&lt;td&gt;All 3 invoices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A view over the table, default settings&lt;/td&gt;
&lt;td&gt;All 3 invoices&lt;/td&gt;
&lt;td&gt;All 3 invoices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rest of this post goes through each row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 1: run the anon-key test on your own app
&lt;/h2&gt;

&lt;p&gt;This is the test a stranger would run. Use it only on an app you own.&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="s1"&gt;'https://YOUR-PROJECT.supabase.co/rest/v1/invoices?select=*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: YOUR_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_ANON_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both values are already in your frontend code. If this returns rows that belong to real users, the table is open to the internet.&lt;/p&gt;

&lt;p&gt;Reads are only half of it. With RLS off, our anonymous test also inserted a new invoice and changed the amount on Bob's invoice from 4,300 to 1. Both requests succeeded. An open table is writable, not just readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: turn RLS on for every table in &lt;code&gt;public&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This query lists every table and whether RLS is on:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any &lt;code&gt;false&lt;/code&gt; is a table the anon key can reach. Turning RLS on is one line:&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="n"&gt;invoices&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;Expect the app to "break" right after. With RLS on and no policies, Postgres denies everything, so Alice also saw an empty list in our test. That's correct behavior. Access now has to be granted on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 3: write policies that name the user
&lt;/h2&gt;

&lt;p&gt;The policy that fixed Billtrack lets a signed-in user read only their own 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;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"read own invoices"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;invoices&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, Alice got her two invoices and the anonymous visitor got nothing. Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;to authenticated&lt;/code&gt; keeps anonymous visitors out of the policy entirely. Without a role, a policy applies to everyone.&lt;/li&gt;
&lt;li&gt;Wrapping &lt;code&gt;auth.uid()&lt;/code&gt; in &lt;code&gt;(select ...)&lt;/code&gt; is Supabase's own performance advice. It lets Postgres evaluate the function once per query instead of once per row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writes need their own policies. With RLS on and only a read policy, Alice's insert was rejected with &lt;code&gt;new row violates row-level security policy&lt;/code&gt;. The insert policy uses &lt;code&gt;with check&lt;/code&gt;, which tests the new row:&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;"insert own invoices"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;invoices&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check 4: look for policies that say &lt;code&gt;true&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When an AI tool hits a permission error, a common "fix" is a policy that allows everything. We added one next to the correct policy:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The anonymous visitor got all three invoices again. Postgres policies are permissive by default, so a row is visible if any policy allows it. One loose policy cancels every careful one.&lt;/p&gt;

&lt;p&gt;Writes have the same trap. With an insert policy of &lt;code&gt;with check (true)&lt;/code&gt;, Alice created an invoice with Bob's &lt;code&gt;user_id&lt;/code&gt;, and it was accepted. With the &lt;code&gt;auth.uid()&lt;/code&gt; version, the same request was rejected.&lt;/p&gt;

&lt;p&gt;This query shows every policy and its condition, so &lt;code&gt;true&lt;/code&gt; stands out:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check 5: check every view
&lt;/h2&gt;

&lt;p&gt;Views are easy to miss. We created a simple view over &lt;code&gt;invoices&lt;/code&gt; and granted it to the API roles, with RLS on and correct policies on the table. The anonymous visitor read all three invoices through the view.&lt;/p&gt;

&lt;p&gt;Supabase's docs explain why: views bypass RLS by default because they are usually created by the &lt;code&gt;postgres&lt;/code&gt; user. On Postgres 15 and later, one setting makes a view obey the table's policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;view&lt;/span&gt; &lt;span class="n"&gt;invoice_totals&lt;/span&gt; &lt;span class="k"&gt;set&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the anonymous visitor got nothing through the view and Alice got her two rows. On older Postgres versions, revoke access to the view from &lt;code&gt;anon&lt;/code&gt; and &lt;code&gt;authenticated&lt;/code&gt;, or move it to a schema the API doesn't expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 6: keep the service role key off the browser
&lt;/h2&gt;

&lt;p&gt;Supabase's secret key maps to the &lt;code&gt;service_role&lt;/code&gt; Postgres role, which has the &lt;code&gt;bypassrls&lt;/code&gt; attribute. In our lab, the service role read all three invoices even with every policy in place. That's by design, for trusted server code.&lt;/p&gt;

&lt;p&gt;It becomes a leak when the key ends up in frontend code. Environment variables with a &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix in Next.js, or &lt;code&gt;VITE_&lt;/code&gt; in Vite, are built into the JavaScript the browser downloads. Search your codebase and your built files for the secret key. If it appears anywhere in client code, rotate it in the Supabase dashboard, then move the code that needs it into a server function.&lt;/p&gt;

&lt;p&gt;Moltbook, an AI-agent social network, shows what one exposed key does. In February 2026, researchers at Wiz reported that a Supabase key in Moltbook's client-side JavaScript gave unauthenticated read and write access to its whole production database, because the tables had no RLS policies. Wiz reported 1.5 million API authentication tokens and about 35,000 email addresses in the exposed data. Once it was reported, the fix was reportedly two SQL statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised us
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;RLS fails silently. When a policy blocked a read or an update, the API returned an empty list with HTTP 200, not an error. An anonymous update to Bob's invoice came back as &lt;code&gt;[]&lt;/code&gt;, status 200, and nothing changed. For a founder testing their own app, "it returns nothing" looks like a bug to fix, and the quickest fix is the &lt;code&gt;using (true)&lt;/code&gt; policy from Check 4.&lt;/li&gt;
&lt;li&gt;The view leak. We expected the view to follow the table's policies. It didn't, until &lt;code&gt;security_invoker&lt;/code&gt; was set.&lt;/li&gt;
&lt;li&gt;Our own test setup lied to us once. We ran a drop-and-create of two policies as one &lt;code&gt;psql&lt;/code&gt; command. The create failed because the policy already existed, so Postgres rolled back the drop too, and the next test passed for the wrong reason. We only caught it because the inserted rows didn't match what we expected. Re-run security tests from a clean state, and check the data, not just the status code.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before an AI-built Supabase app goes live:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the anon-key &lt;code&gt;curl&lt;/code&gt; against every table. Real user data in the response means the table is open.&lt;/li&gt;
&lt;li&gt;Run the &lt;code&gt;pg_tables&lt;/code&gt; query. Every table in &lt;code&gt;public&lt;/code&gt; should show &lt;code&gt;rowsecurity = true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Give every table a policy per operation (select, insert, update, delete) that names a role and checks &lt;code&gt;auth.uid()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the &lt;code&gt;pg_policies&lt;/code&gt; query and remove any policy whose condition is &lt;code&gt;true&lt;/code&gt;, unless the data is meant to be public.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;security_invoker = true&lt;/code&gt; on every view, or revoke API access to it.&lt;/li&gt;
&lt;li&gt;Search client code and build output for the secret key. Rotate it if found.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you'd rather have someone run these checks on your app, &lt;a href="https://inople.com/contact/" rel="noopener noreferrer"&gt;talk to us&lt;/a&gt;. We do this as a fixed-scope audit for apps built with Lovable, Bolt and similar tools.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Is the Supabase anon key a secret?
&lt;/h3&gt;

&lt;p&gt;No. It's designed to be public and is shipped in every frontend that uses Supabase from the browser. Security comes from row level security policies, not from hiding the key.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is CVE-2025-48757?
&lt;/h3&gt;

&lt;p&gt;A vulnerability record for Lovable-generated apps whose Supabase tables could be read or written with only the public anon key, because row level security was missing or incomplete. The researcher found 170 affected apps out of 1,645 scanned. Lovable disputes the CVE, saying each customer is responsible for their own app's data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my app show no data after I enable RLS?
&lt;/h3&gt;

&lt;p&gt;With RLS on and no policies, Postgres denies every row. Add a policy for each operation the app needs, scoped to &lt;code&gt;auth.uid()&lt;/code&gt;. Don't fix it with &lt;code&gt;using (true)&lt;/code&gt;, which opens the table again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Supabase views respect row level security?
&lt;/h3&gt;

&lt;p&gt;Not by default, because views are usually created by the &lt;code&gt;postgres&lt;/code&gt; user and run with its rights. On Postgres 15 and later, set &lt;code&gt;security_invoker = true&lt;/code&gt; on the view.&lt;/p&gt;

&lt;h3&gt;
  
  
  How common are RLS mistakes in Lovable apps?
&lt;/h3&gt;

&lt;p&gt;In our October 2026 review of 38 randomly chosen public Lovable codebases, only 2 of 826 tables had RLS turned off, but 10 of the 38 repositories (26%) had policies letting anyone, or any signed-in user, update or delete every row in at least one table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it safe to use the service role key in the browser?
&lt;/h3&gt;

&lt;p&gt;No. The service role bypasses row level security completely. Keep it in server-side code only.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://inople.com/blog/supabase-rls-checklist-lovable-bolt/" rel="noopener noreferrer"&gt;inople.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Unlock Chrome's Superpowers: Understanding and Building Your First Extension</title>
      <dc:creator>Ayondip Jana</dc:creator>
      <pubDate>Fri, 04 Apr 2025 05:45:25 +0000</pubDate>
      <link>https://dev.to/detronetdip/unlock-chromes-superpowers-understanding-and-building-your-first-extension-3on6</link>
      <guid>https://dev.to/detronetdip/unlock-chromes-superpowers-understanding-and-building-your-first-extension-3on6</guid>
      <description>&lt;p&gt;Google Chrome is more than just a window to the web; it's a powerful platform. And one of its most potent features? &lt;strong&gt;Chrome Extensions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You've probably used them – maybe an ad blocker, a password manager, or a grammar checker. These tiny programs live inside your browser, enhancing your workflow, customizing your experience, and adding functionalities Chrome doesn't have out-of-the-box.&lt;/p&gt;

&lt;p&gt;But have you ever wondered how they work? Or even thought about building one yourself? It might seem daunting, but creating a basic Chrome Extension is surprisingly accessible, even if you're relatively new to web development.&lt;/p&gt;

&lt;p&gt;This post will demystify Chrome Extensions: we'll explore what they are, why they're useful, and then walk you through building your very own simple extension from scratch!&lt;/p&gt;

&lt;h3&gt;
  
  
  What Exactly Are Chrome Extensions?
&lt;/h3&gt;

&lt;p&gt;Think of Chrome Extensions as mini-applications that run within the Chrome browser. They are built using standard web technologies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;HTML:&lt;/strong&gt; For structure (like the popup window).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;CSS:&lt;/strong&gt; For styling (making things look good).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;JavaScript:&lt;/strong&gt; For logic and interaction (making things &lt;em&gt;do&lt;/em&gt; stuff).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extensions can interact with web pages you visit, modify the browser's interface, communicate with web servers, and access various browser APIs (like bookmarks, tabs, history, etc.).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Use (or Build) Them?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Productivity:&lt;/strong&gt; Automate repetitive tasks, integrate different services, manage tabs efficiently.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Customization:&lt;/strong&gt; Change the appearance of websites, add custom keyboard shortcuts, tailor your browsing environment.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Information:&lt;/strong&gt; Quickly access specific data, get definitions, check prices.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Utility:&lt;/strong&gt; Block ads, manage passwords, take notes, check grammar.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fun &amp;amp; Learning:&lt;/strong&gt; Building extensions is a fantastic way to learn web technologies and understand how browsers work under the hood.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Let's Build! Your First Chrome Extension (A Simple Popup)
&lt;/h3&gt;

&lt;p&gt;Ready to dive in? We'll create a very basic extension that shows a simple "Hello, World!" message when you click its icon in the Chrome toolbar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Google Chrome:&lt;/strong&gt; Obviously!&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;A Text Editor:&lt;/strong&gt; Anything from Notepad/TextEdit to VS Code, Sublime Text, or Atom will work.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Basic understanding of HTML (Optional but helpful):&lt;/strong&gt; We'll keep it super simple.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create Your Project Folder&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, create a new folder somewhere on your computer. Let's call it &lt;code&gt;hello-extension&lt;/code&gt;. All the files for our extension will live inside this folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: The Manifest File (&lt;code&gt;manifest.json&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every Chrome Extension &lt;em&gt;must&lt;/em&gt; have a &lt;code&gt;manifest.json&lt;/code&gt; file. This file is the blueprint – it tells Chrome essential information about your extension, like its name, version, and what it does.&lt;/p&gt;

&lt;p&gt;Inside your &lt;code&gt;hello-extension&lt;/code&gt; folder, create a new file named &lt;code&gt;manifest.json&lt;/code&gt; and paste the following code into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello World Extension"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A simple extension that shows a popup."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"16"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon16.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"48"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon48.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"128"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon128.png"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default_popup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"popup.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default_icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"16"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon16.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"48"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon48.png"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Let's break this down:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;manifest_version&lt;/code&gt;: Specifies the manifest file format version. Version 3 is the current standard.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;name&lt;/code&gt;: The name of your extension, shown in the Extensions page and potentially the Chrome Web Store.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;version&lt;/code&gt;: The version number of your extension.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;description&lt;/code&gt;: A short explanation of what your extension does.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;icons&lt;/code&gt;: Specifies icons for your extension in different sizes. Chrome uses these in various places (Extensions page, toolbar, etc.). We'll create placeholder icons soon.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;action&lt;/code&gt;: Defines what happens when the user clicks the extension's icon in the toolbar.

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;default_popup&lt;/code&gt;: Tells Chrome to open &lt;code&gt;popup.html&lt;/code&gt; when the icon is clicked.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;default_icon&lt;/code&gt;: Specifies the icon to display in the toolbar.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Create the Popup HTML (&lt;code&gt;popup.html&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, we need the actual HTML file that will be shown as the popup.&lt;/p&gt;

&lt;p&gt;Inside your &lt;code&gt;hello-extension&lt;/code&gt; folder, create a new file named &lt;code&gt;popup.html&lt;/code&gt; and paste this basic HTML code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Hello!&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;150px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Give the popup a defined width */&lt;/span&gt;
      &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Give the popup a defined height */&lt;/span&gt;
      &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hello,&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Extension!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is just a simple HTML page with a heading and a paragraph. We've added a little inline CSS (&lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;) to give the popup a fixed size and center the text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Create Placeholder Icons&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The manifest file refers to &lt;code&gt;icon16.png&lt;/code&gt;, &lt;code&gt;icon48.png&lt;/code&gt;, and &lt;code&gt;icon128.png&lt;/code&gt;. You need to create these files.&lt;/p&gt;

&lt;p&gt;For this tutorial, you don't need fancy icons. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Find any small PNG image online (respecting licenses!).&lt;/li&gt;
&lt;li&gt;  Use a simple graphics editor (like Paint, GIMP, Photoshop) to create three squares of solid color.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Important:&lt;/strong&gt; Save them as &lt;code&gt;icon16.png&lt;/code&gt; (16x16 pixels), &lt;code&gt;icon48.png&lt;/code&gt; (48x48 pixels), and &lt;code&gt;icon128.png&lt;/code&gt; (128x128 pixels) directly inside your &lt;code&gt;hello-extension&lt;/code&gt; folder. Make sure the names match &lt;em&gt;exactly&lt;/em&gt; what's in the &lt;code&gt;manifest.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your &lt;code&gt;hello-extension&lt;/code&gt; folder should now contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;manifest.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;popup.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;icon16.png&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;icon48.png&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;icon128.png&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Load Your Extension in Chrome&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now for the exciting part – loading your creation into Chrome!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Open Google Chrome.&lt;/li&gt;
&lt;li&gt; Type &lt;code&gt;chrome://extensions&lt;/code&gt; into your address bar and press Enter.&lt;/li&gt;
&lt;li&gt; In the top-right corner of the Extensions page, toggle the &lt;strong&gt;"Developer mode"&lt;/strong&gt; switch &lt;strong&gt;ON&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; New buttons will appear. Click the &lt;strong&gt;"Load unpacked"&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt; A file browser window will open. Navigate to and select your &lt;strong&gt;&lt;code&gt;hello-extension&lt;/code&gt; folder&lt;/strong&gt; (the whole folder, not just the files inside it). Click "Select Folder" or "Open".&lt;/li&gt;
&lt;li&gt; If everything is correct, you should see your "Hello World Extension" card appear on the page! You'll also see its icon (the 48x48 one) appear in your Chrome toolbar (you might need to click the puzzle piece icon to see it and pin it).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Test It!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click the icon for your "Hello World Extension" in the Chrome toolbar.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Voila!&lt;/em&gt; The &lt;code&gt;popup.html&lt;/code&gt; file should appear, displaying your "Hello, Extension!" message.&lt;/p&gt;

&lt;p&gt;Congratulations! You've just built and loaded your very first Chrome Extension!&lt;/p&gt;

&lt;h3&gt;
  
  
  Taking It Further
&lt;/h3&gt;

&lt;p&gt;This was just scratching the surface. Chrome Extensions can do much more complex things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Content Scripts:&lt;/strong&gt; JavaScript files that run in the context of web pages. They can read or modify the DOM (the structure of the page). Think about how grammar checkers highlight text on a page.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Background Scripts (Service Workers in Manifest V3):&lt;/strong&gt; Run in the background, listening for browser events (like a tab updating, a bookmark being created, or the extension icon being clicked if there's no popup).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Options Pages:&lt;/strong&gt; Allow users to customize your extension's settings.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Using Chrome APIs:&lt;/strong&gt; Interact with browser features like tabs, windows, bookmarks, storage, notifications, and much more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where to Learn More:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Official Chrome Developer Documentation:&lt;/strong&gt; &lt;a href="https://developer.chrome.com/docs/extensions/" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/extensions/&lt;/a&gt; - This is your primary resource. Start with the "Getting Started" and "Overview" sections.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Explore Sample Extensions:&lt;/strong&gt; Google provides many examples to learn from.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Experiment!&lt;/strong&gt; Try adding some CSS to style your popup differently, or add some JavaScript to &lt;code&gt;popup.html&lt;/code&gt; to make it interactive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Chrome Extensions are a powerful way to tailor your browsing experience and boost your productivity. While they can become complex, getting started with a simple popup is straightforward using basic web technologies. Hopefully, this guide has demystified the process and inspired you to explore the possibilities.&lt;/p&gt;

&lt;p&gt;What extension ideas do &lt;em&gt;you&lt;/em&gt; have? Share them in the comments below! Happy coding!&lt;/p&gt;




</description>
      <category>webdev</category>
      <category>extensions</category>
      <category>javascript</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
