Third-Party Elimination

By Narain · Supported · self-hosted fonts, google-webfonts-helper, fonttools

Overview

Every third-party resource loaded from an external domain is simultaneously a GDPR liability and a performance burden. The Munich Regional Court ruling (2023) established that loading Google Fonts from Google’s servers transmits the user’s IP address to Google, constituting personal data processing requiring consent. This principle extends to all third-party resources: fonts, maps, embed services, and authentication providers.

The strategy is straightforward: eliminate third-party dependencies by self-hosting or replacing them with privacy-respecting alternatives. This guide covers practical techniques for fonts, embedded content, bot protection, authentication, and email delivery.

Self-Hosted Fonts

Why Third-Party Fonts Are Problematic

When a website loads fonts from Google Fonts or Adobe Typekit:

  1. The browser requests the font from the third-party server.
  2. The third-party server receives the user’s IP address and referrer.
  3. The third-party server can link this information to other requests from the same user.
  4. The user is exposed to fingerprinting based on font requests.

The Munich 2023 ruling confirmed this constitutes personal data processing requiring explicit consent.

Self-Hosting with WOFF2 Format

The solution is self-hosting fonts in WOFF2 format (modern browser support: 98%+).

Advantages:

  • Zero external requests, zero third-party data transmission.
  • WOFF2 compression: typically 50-70% smaller than other formats.
  • Faster loading (no DNS lookup, same-origin requests).
  • No CORS headers required.
  • Full compliance with Condition 6 (Legal Foundation).

Tool 1: google-webfonts-helper

google-webfonts-helper (https://gwfh.mranftl.com/fonts) is a web tool that converts Google Fonts to self-hosted WOFF2 files.

Workflow:

  1. Navigate to google-webfonts-helper.
  2. Search for your desired font (e.g., “Roboto”, “Inter”, “JetBrains Mono”).
  3. Select the weights and styles you need (e.g., 400, 600, 700 for regular, semi-bold, bold).
  4. Copy the CSS snippet provided.
  5. Download the WOFF2 files.
  6. Host files in your /public/fonts/ directory.
  7. Use the provided CSS in your stylesheet.

Example:

@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto-v30-latin-regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto-v30-latin-700.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
}

body {
  font-family: 'Roboto', sans-serif;
}

Implementation note: Always include font-display: swap to prevent layout shifts while fonts load.

Tool 2: fonttools and Subsetting

For advanced use cases where you want to minimize font file sizes, use fonttools (Python-based font manipulation library).

Subsetting is the practice of removing unused glyphs from a font file. If your website uses only Latin characters, subsetting removes Cyrillic, Arabic, CJK, and other character sets, reducing file size by 30-60%.

Installation:

pip install fonttools brotli

Subsetting example:

pyftsubset roboto-regular.ttf --unicodes=U+0000-U+00FF --output-file=roboto-latin.ttf
# Convert TTF to WOFF2
fonttools varLib.instancer roboto-latin.ttf -o roboto-latin.woff2

Tool 3: glyphhanger

glyphhanger (https://github.com/zachleat/glyphhanger) is a Node.js tool that automatically detects which characters your website uses and subsets fonts accordingly.

Workflow:

npm install -g glyphhanger

# Analyze your website and generate subset recommendations
glyphhanger https://example.com --spider --formats=woff2

# Subset a font file to include only detected characters
glyphhanger --whitelist=glyphs.txt input.ttf --output=output.woff2

Advantage: Automatically discovers all characters used across your website, ensuring no missing glyphs while maintaining minimal file size.

Variable Fonts

Variable fonts offer all weights and styles in a single file, eliminating the need for multiple font files.

File size comparison:

  • Roboto (4 weights: 400, 500, 600, 700): ~200KB total (WOFF2)
  • Roboto Variable (same weights in one file): ~60KB (WOFF2)
  • Savings: 70%

CSS:

@font-face {
  font-family: 'Roboto Variable';
  src: url('/fonts/roboto-variable.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
}

body {
  font-family: 'Roboto Variable', sans-serif;
}

h1 {
  font-weight: 700;
}

.light-text {
  font-weight: 300;
}

Recommendation: For new projects, use variable fonts exclusively. For legacy projects, subset to the 4-6 weights actually used on the site.

Facade Patterns for Embedded Content

YouTube Embeds: lite-youtube-embed

Loading a YouTube embed directly requests multiple resources from YouTube servers, transmitting user IP and referrer.

Solution: Use lite-youtube-embed (https://github.com/paulirish/lite-youtube-embed), a minimal wrapper that displays a static thumbnail and only loads the YouTube embed when the user clicks.

Installation:

npm install lite-youtube-embed

HTML:

<lite-youtube videoid="dQw4w9WgXcQ"></lite-youtube>

<script type="module" src="/node_modules/lite-youtube-embed/src/lite-yt-embed.js"></script>
<link rel="stylesheet" href="/node_modules/lite-youtube-embed/src/lite-yt-embed.css">

What happens:

  1. Page loads with a static thumbnail (no YouTube request).
  2. User clicks the video.
  3. YouTube iframe loads only on click.
  4. Performance benefit: deferred YouTube requests until user interaction.
  5. Privacy benefit: no IP/referrer sent to YouTube until user opts in.

If you must embed YouTube without a facade, use youtube-nocookie.com instead of youtube.com:

<iframe
  width="560"
  height="315"
  src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
  frameborder="0"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
  allowfullscreen>
</iframe>

Note: youtube-nocookie.com still transmits user data to YouTube but does not use YouTube cookies. A facade pattern is more privacy-respecting.

Maps: MapLibre GL JS

Loading Google Maps transmits user IP, referrer, and location data to Google.

Alternative: MapLibre GL JS (https://maplibre.org) is an open-source map library that works with open data sources.

Setup:

npm install maplibre-gl

HTML and JavaScript:

<div id="map" style="width: 100%; height: 500px;"></div>

<script type="module">
  import maplibregl from 'maplibre-gl';
  import 'maplibre-gl/dist/maplibre-gl.css';

  const map = new maplibregl.Map({
    container: 'map',
    style: 'https://demotiles.maplibre.org/style.json',
    center: [0, 0],
    zoom: 1
  });

  new maplibregl.Marker()
    .setLngLat([0, 0])
    .addTo(map);
</script>

Data sources:

  • OpenStreetMap (public, free, owned by volunteer community).
  • MapTiler (commercial, proprietary tiles, privacy-respecting).
  • Positron (free tiles from CartoDB, privacy-respecting).

Privacy benefit: No data transmission to Google. All requests go to open-source or privacy-respecting tile providers.

Social Share Buttons: Simple URLs

Loading a Facebook Share Button, LinkedIn Share Button, or Twitter Button transmits data to the respective platform.

Alternative: Static links using share URLs.

Facebook Share:

<a href="https://www.facebook.com/sharer/sharer.php?u=https://example.com">
  Share on Facebook
</a>

LinkedIn Share:

<a href="https://www.linkedin.com/sharing/share-offsite/?url=https://example.com">
  Share on LinkedIn
</a>

Twitter/X Share:

<a href="https://x.com/intent/tweet?url=https://example.com&text=Check%20this%20out">
  Share on X
</a>

Advantage: No third-party script, no data transmission, instant page load.

Bot Protection

Cloudflare Turnstile is a free, invisible CAPTCHA alternative that protects against bot attacks without requiring user interaction.

Advantages:

  • Free tier (unlimited).
  • Invisible (no user interaction in most cases).
  • Can be classified as “strictly necessary” under GDPR (bot protection is a legitimate site security measure).
  • No cookies set by Turnstile itself (though Cloudflare sets its own cookies if using Cloudflare infrastructure).
  • Simple integration.

Implementation:

  1. Sign up for Cloudflare (free tier available).
  2. Register your site and obtain Site Key and Secret Key.
  3. Add Turnstile widget to your form:
<form method="POST">
  <input type="text" name="email" required>
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
  <button type="submit">Subscribe</button>
</form>

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
  1. On the server, verify the Turnstile token:
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/api/siteverify', {
  method: 'POST',
  body: JSON.stringify({
    secret: process.env.TURNSTILE_SECRET_KEY,
    response: req.body.token
  })
});

const data = await response.json();
if (data.success) {
  // Process form submission
}

Classification: Turnstile can be classified as “strictly necessary” because it protects the site from automated attacks, not for marketing or analytics.

ALTCHA (Self-Hosted Alternative)

ALTCHA (https://altcha.com) is a self-hosted, zero-external-dependency bot protection system based on proof-of-work.

Advantages:

  • Self-hosted (no external requests to ALTCHA servers).
  • Proof-of-work based (user’s browser solves a computational puzzle).
  • No JavaScript fingerprinting or tracking.
  • GDPR compliant by design.
  • Open source.

Setup:

npm install altcha

HTML:

<form id="form">
  <input type="email" name="email" required>
  <div id="altcha"></div>
  <button type="submit">Subscribe</button>
</form>

<script type="module">
  import { Altcha } from 'altcha';

  const altcha = new Altcha({
    element: '#altcha',
    challengeurl: '/api/altcha-challenge',
    onVerify: (token) => {
      // Set hidden input with verification token
      document.querySelector('input[name="_altcha"]').value = token;
    }
  });
</script>

Server-side verification:

import { verifySolution } from 'altcha';

const isValid = await verifySolution(req.body.altchaToken, {
  hmacKey: process.env.ALTCHA_HMAC_KEY
});

if (isValid) {
  // Process form
}

Recommendation: For sites prioritizing privacy and self-hosting, ALTCHA is superior. For simplicity and free tier, Turnstile is recommended.

Honeypot Fields

A simple, zero-JavaScript bot protection technique: honeypot fields are hidden form fields that legitimate users cannot see but bots will fill in.

HTML:

<form method="POST" action="/subscribe">
  <input type="email" name="email" required>

  <!-- Honeypot field -->
  <input type="text" name="website" style="position: absolute; left: -9999px;">

  <button type="submit">Subscribe</button>
</form>

Server validation:

if (req.body.website) {
  // Bot detected (honeypot field was filled)
  return res.status(400).send('Invalid submission');
}

// Legitimate submission, process normally

Advantages:

  • No external services, no API calls, no tracking.
  • Zero performance impact.
  • Very effective (bots cannot distinguish honeypot fields).
  • GDPR compliant.

Limitation: Does not protect against sophisticated attacks that analyze the DOM. Best used in combination with Turnstile or ALTCHA.

Authentication Without Third Parties

WebAuthn and FIDO2

WebAuthn (Web Authentication) and FIDO2 (Fast Identity Online) enable passwordless authentication using hardware keys, biometrics, or device PIN.

Advantages:

  • No password transmission, no password storage on server.
  • No third-party authentication provider required (unlike Google OAuth, GitHub OAuth).
  • Phishing-resistant (keys only work with the registered domain).
  • Privacy-respecting (no data shared with external authentication providers).
  • Supports hardware security keys (YubiKey, etc.), Touch ID, Windows Hello, Android biometrics.

Implementation (Node.js with SimpleWebAuthn library):

npm install @simplewebauthn/server @simplewebauthn/browser

Server setup:

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';

// Registration: User creates a new passkey
app.post('/register/options', async (req, res) => {
  const options = await generateRegistrationOptions({
    rpID: 'example.com',
    rpName: 'Example Site',
    userID: userId,
    userName: userEmail,
    userDisplayName: userName,
    attestationType: 'direct'
  });

  // Store challenge temporarily
  req.session.registrationChallenge = options.challenge;

  res.json(options);
});

app.post('/register/verify', async (req, res) => {
  const verification = await verifyRegistrationResponse({
    response: req.body,
    expectedChallenge: req.session.registrationChallenge,
    expectedOrigin: 'https://example.com',
    expectedRPID: 'example.com'
  });

  // Store credential public key in database
  if (verification.verified) {
    // Save user's credential
  }

  res.json({ verified: verification.verified });
});

Client-side:

import {
  startRegistration,
  startAuthentication
} from '@simplewebauthn/browser';

// Registration
const credentialCreationOptions = await fetch('/register/options').then(r => r.json());
const credential = await startRegistration(credentialCreationOptions);
await fetch('/register/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(credential)
});

// Authentication
const authenticationOptions = await fetch('/login/options').then(r => r.json());
const assertion = await startAuthentication(authenticationOptions);
const verificationResult = await fetch('/login/verify', {
  method: 'POST',
  body: JSON.stringify(assertion)
}).then(r => r.json());

if (verificationResult.verified) {
  // User is authenticated
}

Clerk (Third-Party, But Privacy-Respecting)

If you need a managed authentication service without building WebAuthn yourself:

Clerk (https://clerk.com) offers:

  • Passwordless authentication (email, SMS, passkeys).
  • GDPR and SOC 2 compliant.
  • Simple integration (no data sharing with third parties for analytics/profiling).
  • Explicit data processing agreement.

Alternative: Supabase Auth (open-source, self-hostable).

Email Delivery

Third-Party Email Services and Privacy

Services like Mailchimp set tracking pixels on every email, transmitting user IP and read status back to Mailchimp servers. This requires GDPR compliance and explicit consent in many jurisdictions.

Buttondown (Privacy-Respecting)

Buttondown (https://buttondown.email) is a privacy-first email newsletter platform:

  • No tracking pixels by default.
  • No email open tracking.
  • No click tracking.
  • Simple, transparent pricing.
  • GDPR compliant.

Integration: Simple one-line subscription form:

<form action="https://buttondown.email/api/emails/embed-subscribe/username" method="post">
  <input type="email" name="email" required>
  <button type="submit">Subscribe</button>
</form>

Listmonk (Self-Hosted)

Listmonk (https://listmonk.app) is an open-source, self-hosted email newsletter and mailing list platform:

  • Complete control over email data.
  • No third-party tracking.
  • Full GDPR compliance capabilities (unsubscribe, data export, deletion).
  • Lightweight and simple to self-host.

Docker deployment:

docker run -d -p 9000:9000 listmonk/listmonk:latest

Security Headers for Third-Party Control

Even with third-party elimination, enforce headers that block unexpected external requests:

Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; script-src 'self' https://challenges.cloudflare.com; frame-ancestors 'none'

This CSP blocks:

  • All external scripts except Turnstile.
  • All external fonts.
  • All external images except data URIs and HTTPS.
  • All external stylesheets.

Verification: Use CSP violation reporting to detect attempts to load external resources:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /api/csp-report

Audit Checklist

  • Audit all external scripts loaded on your site (DevTools Network tab).
  • Replace Google Fonts with self-hosted WOFF2 fonts.
  • Replace YouTube embeds with lite-youtube-embed.
  • Replace Google Maps with MapLibre GL JS.
  • Replace social buttons with static links.
  • Remove third-party analytics (Google Analytics, Mixpanel, etc.).
  • Implement Cloudflare Turnstile or ALTCHA for bot protection.
  • Implement WebAuthn for authentication if possible.
  • Switch to privacy-respecting email platform (Buttondown or Listmonk).
  • Set Content-Security-Policy headers to block unexpected external requests.
  • Verify no external requests in browser DevTools Network tab.

References


Related Guides

  • The Legal Foundation — Condition 6 requires elimination of third-party resources that transmit user data
  • The Performance Dividend — Self-hosting and elimination of third-party scripts recovers 200-500KB of payload and improves Core Web Vitals
  • The Framework Stack — Cloudflare WAF and security headers enforce third-party elimination at the edge