<?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>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>
