<?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: Hannes</title>
    <description>The latest articles on DEV Community by Hannes (@hdevv).</description>
    <link>https://dev.to/hdevv</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%2F1600321%2F9b4b6315-f207-4e36-978d-641b63559cd4.png</url>
      <title>DEV Community: Hannes</title>
      <link>https://dev.to/hdevv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hdevv"/>
    <language>en</language>
    <item>
      <title>I scanned 55 public Lovable apps — all 55 had a GDPR or security finding</title>
      <dc:creator>Hannes</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:06:12 +0000</pubDate>
      <link>https://dev.to/hdevv/i-scanned-55-public-lovable-apps-all-55-had-a-gdpr-or-security-finding-p1h</link>
      <guid>https://dev.to/hdevv/i-scanned-55-public-lovable-apps-all-55-had-a-gdpr-or-security-finding-p1h</guid>
      <description>&lt;p&gt;AI app builders are genuinely remarkable. You describe an app, and minutes later it is deployed on a real URL with a real database. That part works.&lt;/p&gt;

&lt;p&gt;What does not come along is the boring layer underneath: security headers, consent timing, legal pages. Nobody prompts for those, so nobody gets them.&lt;/p&gt;

&lt;p&gt;I wanted to know how large that gap actually is, so I measured it. (Practical guidance, not legal advice.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did
&lt;/h2&gt;

&lt;p&gt;I collected &lt;strong&gt;55 publicly discoverable apps on &lt;code&gt;*.lovable.app&lt;/code&gt;&lt;/strong&gt; — trackers, invoicing tools, portfolios, CRM dashboards, fitness apps, waitlist pages — and ran a &lt;strong&gt;passive&lt;/strong&gt; check on each.&lt;/p&gt;

&lt;p&gt;Passive matters. Every app was loaded once over HTTPS with plain GET requests, exactly the way any visitor's browser loads it. No login attempts, no active probing, no guessing at hidden paths. Everything below is what these apps tell the entire internet on first page load.&lt;/p&gt;

&lt;p&gt;Results are aggregate only. &lt;strong&gt;No app is named&lt;/strong&gt;, and none will be. The full breakdown lives on &lt;a href="https://howsafeismyapp.com/research" rel="noopener noreferrer"&gt;the research page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;Out of 55 apps, &lt;strong&gt;every single one had at least one finding&lt;/strong&gt; — on average 4.3 each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;98% have no Content-Security-Policy&lt;/strong&gt; (54 of 55)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;98% have no clickjacking protection&lt;/strong&gt; (54 of 55)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;95% embed a third-party tracker&lt;/strong&gt; (52 of 55)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;95% load tracking before any consent&lt;/strong&gt; (52 of 55)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;45% hotlink Google Fonts from Google&lt;/strong&gt; (25 of 55)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not one clean result in the sample. That was the part I did not expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each finding actually means
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No Content-Security-Policy.&lt;/strong&gt; A &lt;a href="https://howsafeismyapp.com/checks/content-security-policy" rel="noopener noreferrer"&gt;CSP&lt;/a&gt; tells the browser which scripts may run. Without it, any script that gets injected into your page — through a compromised dependency, a vulnerable input, a malicious extension — executes with full access to everything your users type. It is the strongest browser-side defense against XSS, and virtually nothing in this sample had one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No clickjacking protection.&lt;/strong&gt; Without &lt;a href="https://howsafeismyapp.com/checks/clickjacking-protection" rel="noopener noreferrer"&gt;X-Frame-Options or frame-ancestors&lt;/a&gt;, anyone can embed your app in a hidden iframe, overlay it with fake UI, and trick a logged-in user into clicking your real buttons. Delete account. Confirm payment. They never see what they clicked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trackers before consent.&lt;/strong&gt; This is the legal one. Under Germany's &lt;strong&gt;§ 25 TDDDG&lt;/strong&gt; and the GDPR, consent has to come &lt;em&gt;first&lt;/em&gt;. A banner that appears after analytics already fired protects nobody — the data left on page load. In this sample, 52 of 55 apps started sending data to third parties before the visitor could possibly agree. Background: &lt;a href="https://howsafeismyapp.com/checks/tracking-without-consent" rel="noopener noreferrer"&gt;tracking without consent&lt;/a&gt; and &lt;a href="https://howsafeismyapp.com/checks/third-party-tracker" rel="noopener noreferrer"&gt;what a third-party tracker is&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Fonts hotlinked.&lt;/strong&gt; Loading fonts from &lt;code&gt;fonts.googleapis.com&lt;/code&gt; sends every visitor's IP address to Google. A German court (LG München I, 2022) awarded damages over exactly this, and it started a wave of warning letters. Nearly half the sample still does it — and it is the easiest fix on the list. Details: &lt;a href="https://howsafeismyapp.com/checks/google-fonts-gdpr" rel="noopener noreferrer"&gt;Google Fonts and the GDPR&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why builders ship this way
&lt;/h2&gt;

&lt;p&gt;None of this is Lovable being careless. It is structural, and it applies to Bolt, v0, Replit and every other builder equally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Security headers are invisible.&lt;/strong&gt; Nothing looks different when they are missing. No error, no warning — the app just works, right until it does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templates carry defaults.&lt;/strong&gt; Analytics snippets and font links ship with the starter code. They work, so nobody removes them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody prompts for compliance.&lt;/strong&gt; You ask for "a booking app with Stripe", not "and a CSP header and a consent gate". Why would you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The obligation is yours regardless.&lt;/strong&gt; If people in the EU use your app, you are the operator under &lt;strong&gt;Art. 3(2) GDPR&lt;/strong&gt; — wherever you are based, whoever generated the code.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The fixes, in the order I would do them
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Security headers.&lt;/strong&gt; Mostly one config file. &lt;code&gt;X-Frame-Options&lt;/code&gt; and &lt;code&gt;nosniff&lt;/code&gt; are one-liners with near-zero breakage risk. Add &lt;a href="https://howsafeismyapp.com/checks/hsts" rel="noopener noreferrer"&gt;HSTS&lt;/a&gt; once everything runs on HTTPS. Do CSP last, in report-only mode, tightening until the console is quiet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-host your fonts.&lt;/strong&gt; Download the &lt;code&gt;.woff2&lt;/code&gt; files, serve them from your own domain, delete the Google link. About 15 minutes — the walkthrough is in &lt;a href="https://howsafeismyapp.com/blog/self-host-google-fonts" rel="noopener noreferrer"&gt;self-hosting Google Fonts&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix consent timing.&lt;/strong&gt; Either remove the non-essential trackers (then you may need no banner at all), or route every tracking script through a consent tool's blocking mode so nothing fires until opt-in. We covered which apps actually need one in &lt;a href="https://howsafeismyapp.com/blog/cookie-banner-lovable-app" rel="noopener noreferrer"&gt;do you need a cookie banner for your Lovable app&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write real legal pages.&lt;/strong&gt; A &lt;a href="https://howsafeismyapp.com/checks/missing-privacy-policy" rel="noopener noreferrer"&gt;privacy policy&lt;/a&gt; naming the services you actually use. Generic generator text listing tools you do not have is its own liability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this requires rebuilding your app. For most projects it is an afternoon of configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The caveats
&lt;/h2&gt;

&lt;p&gt;This is a &lt;strong&gt;convenience sample&lt;/strong&gt;, not a random draw — 55 apps I could discover publicly. Read the percentages as directional, not exact. A different 55 would land nearby, not identical.&lt;/p&gt;

&lt;p&gt;I also deliberately excluded findings that are &lt;strong&gt;public by design&lt;/strong&gt;. A Supabase anon key appears in the frontend of essentially every Lovable app, and that is how it is meant to work — it is protected by row-level security, not by secrecy. Counting it as a "leak" would inflate the numbers and mislead people, so it is out. Only documented security and GDPR checks are counted. (The genuinely dangerous Supabase findings — a service-role key in the bundle, or a table readable without login — are in scope. Neither appeared in this sample.)&lt;/p&gt;

&lt;h2&gt;
  
  
  See where your app stands
&lt;/h2&gt;

&lt;p&gt;The complete data, with every finding linked to what it means and how to fix it, is on &lt;a href="https://howsafeismyapp.com/research" rel="noopener noreferrer"&gt;the research page&lt;/a&gt;. If you want the same check on your own app, it runs in about a minute, passively, with no login: see &lt;a href="https://howsafeismyapp.com/for/lovable" rel="noopener noreferrer"&gt;what we check on Lovable apps&lt;/a&gt;, or run the &lt;a href="https://howsafeismyapp.com/tools/cookie-tracker-scanner" rel="noopener noreferrer"&gt;cookie and tracker scanner&lt;/a&gt; on its own.&lt;/p&gt;

</description>
      <category>gdpr</category>
      <category>security</category>
      <category>webdev</category>
      <category>nocode</category>
    </item>
    <item>
      <title>Self-hosting Google Fonts: the 15-minute fix for a classic GDPR finding</title>
      <dc:creator>Hannes</dc:creator>
      <pubDate>Tue, 11 Aug 2026 21:52:15 +0000</pubDate>
      <link>https://dev.to/hdevv/self-hosting-google-fonts-the-15-minute-fix-for-a-classic-gdpr-finding-mjd</link>
      <guid>https://dev.to/hdevv/self-hosting-google-fonts-the-15-minute-fix-for-a-classic-gdpr-finding-mjd</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://howsafeismyapp.com/blog/self-host-google-fonts" rel="noopener noreferrer"&gt;howsafeismyapp.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your app's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; contains a line like&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet"&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;then every visitor's browser contacts Google's servers before your page even renders — transmitting their IP address along the way. In 2022, a German court (LG München I) ruled that exactly this, done without consent, violates visitors' rights, and awarded damages to a website visitor. The decision triggered a wave of warning letters (Abmahnungen) against site operators, aimed precisely at small sites that had copied the standard embed code.&lt;/p&gt;

&lt;p&gt;It remains one of the most common findings on AI-built apps, because font embeds are baked into templates and AI-generated layouts. It is also one of the easiest to fix. (Practical guidance, not legal advice.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a GDPR issue at all
&lt;/h2&gt;

&lt;p&gt;An IP address is personal data. Sending it to a third party requires a legal basis (&lt;strong&gt;Art. 6 GDPR&lt;/strong&gt;), and transfers to US providers raise the additional third-country questions of &lt;strong&gt;Art. 44 GDPR&lt;/strong&gt;. "The CSS loads faster from Google's CDN" is not a legal basis — especially since self-hosting is equivalent in practice: fonts served from your own domain are cached, local, and remove a DNS lookup and TLS handshake to a foreign origin. The details are on our check page: &lt;a href="https://howsafeismyapp.com/checks/google-fonts-gdpr" rel="noopener noreferrer"&gt;Google Fonts loaded directly from Google&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Fonts are the famous example, but the same logic applies to anything your page pulls from third-party CDNs on first load — icon fonts, CSS frameworks, JS libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find out if your app is affected
&lt;/h2&gt;

&lt;p&gt;Fastest way: our free &lt;a href="https://howsafeismyapp.com/tools/google-fonts-checker" rel="noopener noreferrer"&gt;Google Fonts checker&lt;/a&gt; answers this in seconds. Manually: open your app in a private window with DevTools → Network, reload, and filter for &lt;code&gt;fonts.googleapis.com&lt;/code&gt; or &lt;code&gt;fonts.gstatic.com&lt;/code&gt;. In a Lovable/Bolt/v0 project you can also just search the code for &lt;code&gt;googleapis&lt;/code&gt; — the embed usually sits in &lt;code&gt;index.html&lt;/code&gt; or a global CSS file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: serve the fonts yourself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Download the font files.&lt;/strong&gt; The open-source tool &lt;em&gt;google-webfonts-helper&lt;/em&gt; packages any Google Font as &lt;code&gt;.woff2&lt;/code&gt; files with ready-made CSS — or download from the font's official repository. &lt;code&gt;.woff2&lt;/code&gt; alone is enough for every current browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the files in your app's public/static folder&lt;/strong&gt;, e.g. &lt;code&gt;public/fonts/inter-v13-latin-regular.woff2&lt;/code&gt;. In Lovable, add them to the project's public assets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace the Google &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; with local &lt;code&gt;@font-face&lt;/code&gt; rules:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&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="s1"&gt;"Inter"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("/fonts/inter-v13-latin-regular.woff2")&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"woff2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Delete the &lt;code&gt;fonts.googleapis.com&lt;/code&gt; link tag&lt;/strong&gt; — this step is the actual fix; the scanner (and the warning-letter industry) looks for the request, not your intentions.&lt;/li&gt;
&lt;li&gt;Reload with the Network tab open: there should be &lt;strong&gt;no request leaving your domain&lt;/strong&gt; for fonts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total effort for a typical two-font app: about 15 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two upgrades while you're at it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long cache lifetimes for font files.&lt;/strong&gt; They never change; a year-long &lt;code&gt;Cache-Control&lt;/code&gt; header makes repeat visits faster than the Google CDN ever was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the rest of your first load.&lt;/strong&gt; If fonts came from a third party, other resources often do too. Loading anything over plain HTTP is its own finding — &lt;a href="https://howsafeismyapp.com/checks/mixed-content" rel="noopener noreferrer"&gt;resources loaded over unencrypted connections&lt;/a&gt; — and third-party &lt;em&gt;tracking&lt;/em&gt; scripts are a much bigger consent problem than fonts: &lt;a href="https://howsafeismyapp.com/checks/tracking-without-consent" rel="noopener noreferrer"&gt;tracking loads before anyone could consent&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verify from the outside
&lt;/h2&gt;

&lt;p&gt;The nice thing about this class of problem: it is fully visible from the outside, no access to your code needed. Our free passive scan requests your app like any anonymous browser would and reports every third-party request on first load — fonts included — plus 14 other checks. Takes about a minute; nothing gets stored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;font-display: swap&lt;/code&gt; or preconnect help legally?
&lt;/h3&gt;

&lt;p&gt;No. Those are performance tweaks — the legal issue is the request to Google's servers itself, which transmits the visitor's IP address. Only removing the request fixes it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about Bootstrap, jQuery or icon CDNs?
&lt;/h3&gt;

&lt;p&gt;Same logic. Any resource loaded from a third-party CDN on first paint transmits visitor IPs to that provider without a legal basis. Self-host what you can; whatever remains needs a justification and a mention in your privacy policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this a fine risk or a warning-letter risk?
&lt;/h3&gt;

&lt;p&gt;In practice, warning letters and small damages claims — the 2022 Munich decision awarded a visitor damages, and copycat claims followed in waves. The cost of a single response letter to a lawyer exceeds the cost of self-hosting many times over. If you're doing a broader consent cleanup anyway, start here: &lt;a href="https://howsafeismyapp.com/blog/cookie-banner-lovable-app" rel="noopener noreferrer"&gt;Do you need a cookie banner?&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>gdpr</category>
      <category>webdev</category>
      <category>performance</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Row Level Security in Lovable apps: why your database might be public</title>
      <dc:creator>Hannes</dc:creator>
      <pubDate>Tue, 11 Aug 2026 21:49:44 +0000</pubDate>
      <link>https://dev.to/hdevv/row-level-security-in-lovable-apps-why-your-database-might-be-public-mj2</link>
      <guid>https://dev.to/hdevv/row-level-security-in-lovable-apps-why-your-database-might-be-public-mj2</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://howsafeismyapp.com/blog/lovable-supabase-rls-guide" rel="noopener noreferrer"&gt;howsafeismyapp.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is the uncomfortable truth about most Lovable apps: the database is not behind your login form. It is behind Supabase's API — and whether that API hands out your data to strangers depends on one setting most AI-generated apps never mention: &lt;strong&gt;Row Level Security&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the most serious finding our scanner sees in the wild. It is also completely fixable in an afternoon. (Practical guidance, not legal advice.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your frontend talks to the database directly
&lt;/h2&gt;

&lt;p&gt;Traditional apps put a backend server between the browser and the database. Lovable apps usually don't: the browser talks to Supabase's REST API directly, authenticated with the so-called &lt;code&gt;anon&lt;/code&gt; key that is shipped inside your JavaScript bundle.&lt;/p&gt;

&lt;p&gt;That key is &lt;em&gt;supposed&lt;/em&gt; to be public. Anyone who opens your app has it — and with it, anyone can send requests like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET https://your-project.supabase.co/rest/v1/users?select=*&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What decides whether that request returns your entire users table or an empty response is not the key. It is Row Level Security.&lt;/p&gt;

&lt;h2&gt;
  
  
  What RLS actually does
&lt;/h2&gt;

&lt;p&gt;Row Level Security is a PostgreSQL feature that Supabase builds on. With RLS enabled on a table, every query is filtered through &lt;strong&gt;policies&lt;/strong&gt; — rules that say which rows a given user may read or write. A typical policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a user may &lt;code&gt;SELECT&lt;/code&gt; rows in &lt;code&gt;profiles&lt;/code&gt; where &lt;code&gt;user_id = auth.uid()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;nobody may &lt;code&gt;SELECT&lt;/code&gt; from &lt;code&gt;payments&lt;/code&gt; at all via the public API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Without RLS enabled, there is no filter.&lt;/strong&gt; The &lt;code&gt;anon&lt;/code&gt; key can read every row of every exposed table. Your login form is decoration; the data was never behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI-built apps get this wrong
&lt;/h2&gt;

&lt;p&gt;Lovable and similar builders generate working code fast, and "working" means the app can read and write its own data. Two common paths to trouble:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The generated setup &lt;strong&gt;never enables RLS&lt;/strong&gt; on new tables, because everything works without it in development.&lt;/li&gt;
&lt;li&gt;RLS was enabled, but a debugging session ended with a policy like &lt;code&gt;USING (true)&lt;/code&gt; — "allow everyone" — that never got removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither produces an error. The app works perfectly. That is exactly why this finding survives into production. Our check page explains what the scanner looks for: &lt;a href="https://howsafeismyapp.com/checks/open-database" rel="noopener noreferrer"&gt;Database readable without a login&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own app in two minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open your app in the browser, then open DevTools → Network.&lt;/li&gt;
&lt;li&gt;Reload and look for requests to &lt;code&gt;*.supabase.co/rest/v1/...&lt;/code&gt; — note your project URL and the &lt;code&gt;apikey&lt;/code&gt; header (that's the anon key; it's meant to be visible).&lt;/li&gt;
&lt;li&gt;In a private browser window — logged out of everything — request one of your tables with that URL and key, for example with &lt;code&gt;curl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you get data back while logged out, that table is public. Anything with names, emails, or user content is now a data-protection incident waiting to be discovered by someone less friendly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Under &lt;strong&gt;Art. 32 GDPR&lt;/strong&gt; you are required to have appropriate technical measures for personal data; an openly readable users table is about as clear a failure of that as exists. If real personal data was exposed, &lt;strong&gt;Art. 33 GDPR&lt;/strong&gt; (breach notification, 72 hours) becomes the relevant question — a conversation you want to avoid ever needing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Locking it down
&lt;/h2&gt;

&lt;p&gt;In the Supabase dashboard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Table editor → your table → enable RLS.&lt;/strong&gt; Do this for every table in the public schema. Supabase warns you about tables without RLS — take the warning seriously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write policies from the user's perspective.&lt;/strong&gt; Start restrictive: users read their own rows (&lt;code&gt;auth.uid() = user_id&lt;/code&gt;), insert only their own, update only their own. Add broader read access only where the product genuinely needs it (e.g. a public feed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tables the browser never needs&lt;/strong&gt; (internal logs, admin data) get RLS enabled with &lt;strong&gt;no policies at all&lt;/strong&gt; — that means no access via the public API, which is exactly right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never ship the &lt;code&gt;service_role&lt;/code&gt; key to the frontend.&lt;/strong&gt; It bypasses RLS entirely. If it appears anywhere in your bundle, rotate it now — we cover this separately: &lt;a href="https://howsafeismyapp.com/checks/exposed-admin-key" rel="noopener noreferrer"&gt;Admin key exposed in frontend code&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then re-run the two-minute check from above. The logged-out request should now return an empty array or an error — both are success.&lt;/p&gt;

&lt;h2&gt;
  
  
  After the fix: verify from the outside
&lt;/h2&gt;

&lt;p&gt;The inside view (dashboard says RLS enabled) and the outside view (what a stranger's browser can actually fetch) can disagree — a forgotten table, a too-broad policy, a second Supabase project. The outside view is the one that matters, and it is what a passive scan checks: our scanner requests your app the way any anonymous visitor would and reports which tables answer. It takes about a minute, needs no login, and stores nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

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

&lt;p&gt;No — it is designed to be public and ships in your JavaScript bundle by design. The protection layer is Row Level Security, not the key. The key that must stay secret is &lt;code&gt;service_role&lt;/code&gt; — see &lt;a href="https://howsafeismyapp.com/checks/exposed-admin-key" rel="noopener noreferrer"&gt;admin key exposed in frontend code&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will enabling RLS break my app?
&lt;/h3&gt;

&lt;p&gt;Enabling RLS without policies makes tables return empty results via the public API — so your app's own queries stop working until you add policies for them. That is the point: write the policies your app needs, and everything else stays closed. Budget an hour, not a weekend.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know whether someone already accessed my data?
&lt;/h3&gt;

&lt;p&gt;Check the API logs in the Supabase dashboard for anonymous requests to sensitive tables. If personal data of real users was readable, look into the breach-notification duties of Art. 33 GDPR — the 72-hour window starts when you become aware, and honest, early handling counts in your favor.&lt;/p&gt;

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