Index: src/middleware/render.js
===================================================================
--- src/middleware/render.js	(revision a9e23f60c1eb494375ea79b53c1bdc9914deae61)
+++ src/middleware/render.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
@@ -18,4 +18,5 @@
 import { isViewer } from './auth.js';
 import { getSetting } from '../services/SettingsService.js';
+import { isPremium as isPremiumInstance, premiumEnabled } from '../services/PatreonService.js';
 import { PLATFORMS as PLATFORMS_CATALOG } from '../services/PlatformIcons.js';
 
@@ -77,4 +78,6 @@
     isViewer: _isViewer,
     canMutate: !_isViewer,
+    isPremium: isPremiumInstance(),
+    premiumEnabled: premiumEnabled(),
     siteOwnerAvatar,
     site: _site,
Index: src/routes/admin-patreon.js
===================================================================
--- src/routes/admin-patreon.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
+++ src/routes/admin-patreon.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
@@ -0,0 +1,59 @@
+/**
+ * Admin: Patreon koppelen voor de premium-laag (god-only).
+ *
+ * GET /admin/patreon/connect    -> stuur de beheerder naar de license-server
+ *                                  (oauth/start) met onze callback als return.
+ * GET /admin/patreon/callback   -> license-server keert terug met ?klonkt_token
+ *                                  (of ?klonkt_error). Verifieer + sla op.
+ * GET /admin/patreon/disconnect -> entitlement wissen.
+ *
+ * Het echte verdienmodel-slot zit in het ondertekende token (alleen de
+ * license-server kan tekenen). Zie PatreonService.js.
+ */
+
+import express from 'express';
+import { requireGod } from '../middleware/auth.js';
+import {
+  licenseBase, premiumEnabled, verifyEntitlementToken, storeEntitlement, clearEntitlement,
+} from '../services/PatreonService.js';
+
+const router = express.Router();
+router.use(requireGod);
+
+function baseUrl(req) {
+  return (process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`).replace(/\/$/, '');
+}
+
+router.get('/connect', (req, res) => {
+  if (!premiumEnabled()) return res.redirect('/admin/settings');
+  const ret = baseUrl(req) + '/admin/patreon/callback';
+  res.redirect(`${licenseBase()}/oauth/start?return=${encodeURIComponent(ret)}`);
+});
+
+router.get('/callback', async (req, res) => {
+  if (!premiumEnabled()) return res.redirect('/admin/settings');
+  const { klonkt_token, klonkt_error, klonkt_support_cents } = req.query;
+  if (klonkt_error) {
+    const cents = Number(klonkt_support_cents || 0);
+    const msg = klonkt_error === 'not_entitled'
+      ? `Patreon gekoppeld, maar nog geen $10 lifetime (nu $${(cents / 100).toFixed(2)}). Steun de campagne en koppel opnieuw.`
+      : 'Patreon-koppeling mislukt.';
+    return res.redirect('/admin/settings?error=' + encodeURIComponent(msg));
+  }
+  try {
+    const payload = await verifyEntitlementToken(String(klonkt_token || ''));
+    if (!payload.entitled) throw new Error('not entitled');
+    storeEntitlement(payload, String(klonkt_token));
+    res.redirect('/admin/settings?success=' + encodeURIComponent('Patreon gekoppeld — premium is actief.'));
+  } catch (e) {
+    console.error('[patreon/callback]', e.message);
+    res.redirect('/admin/settings?error=' + encodeURIComponent('Patreon-token kon niet geverifieerd worden.'));
+  }
+});
+
+router.get('/disconnect', (req, res) => {
+  clearEntitlement();
+  res.redirect('/admin/settings?success=' + encodeURIComponent('Patreon ontkoppeld.'));
+});
+
+export default router;
Index: src/routes/admin-settings.js
===================================================================
--- src/routes/admin-settings.js	(revision a9e23f60c1eb494375ea79b53c1bdc9914deae61)
+++ src/routes/admin-settings.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
@@ -22,4 +22,5 @@
 import { requireGod } from '../middleware/auth.js';
 import { getTenancy, setTenancy, getSetting, setSetting } from '../services/SettingsService.js';
+import { entitlementStatus } from '../services/PatreonService.js';
 
 const router = express.Router();
@@ -74,4 +75,5 @@
     hubHeroImage: getSetting('hub_hero_image') || '',
     hubHeroOverlay: clampOverlay(getSetting('hub_hero_overlay')),
+    premium: entitlementStatus(),
     success: req.query.success || null,
     error: req.query.error || null,
Index: src/server.js
===================================================================
--- src/server.js	(revision a9e23f60c1eb494375ea79b53c1bdc9914deae61)
+++ src/server.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
@@ -48,4 +48,5 @@
 import adminCircleRoutes from './routes/admin-circle.js';
 import adminUpdatesRoutes from './routes/admin-updates.js';
+import adminPatreonRoutes from './routes/admin-patreon.js';
 import circleRoutes from './routes/circle.js';
 
@@ -239,4 +240,5 @@
 app.use('/admin/circle', adminCircleRoutes);
 app.use('/admin/updates', adminUpdatesRoutes);
+app.use('/admin/patreon', adminPatreonRoutes);
 app.use('/admin', adminRoutes);
 app.use('/prutter', prutterRoutes);
Index: src/services/PatreonService.js
===================================================================
--- src/services/PatreonService.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
+++ src/services/PatreonService.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
@@ -0,0 +1,93 @@
+// Patreon-entitlement (premium-laag).
+//
+// Model (Klonkt, 2026-06): de app + alle updates zijn gratis. Een paar premium-
+// modules (Hub-modus, Statistieken, Fan-login) zitten achter een $10-lifetime
+// Patreon-supporter-status. De centrale license-server (license.klonkt.com)
+// checkt Patreon en tekent een Ed25519-JWT "entitlement-token". DEZE instance
+// verifieert dat token OFFLINE met de publieke sleutel van de server — een
+// gekraakte/geforkte self-host kan dus geen geldig token verzinnen (alleen de
+// license-server kan tekenen). Dat is het echte slot; de feature-flags zelf zijn
+// op self-host wel te patchen (bewust geaccepteerd: $10 < moeite om te kraken).
+//
+// Premium staat STANDAARD UIT (KLONKT_PREMIUM_ENABLED != 'on'): dan is er geen
+// premium-UI en wordt er niets gegate. De self-hoster zet 'm aan zodra Patreon
+// geregeld is.
+
+import crypto from 'node:crypto';
+import { getSetting, setSetting } from './SettingsService.js';
+
+const LICENSE_URL = (process.env.KLONKT_LICENSE_URL || 'https://license.klonkt.com').replace(/\/$/, '');
+const ISSUER = 'klonkt-license';
+
+export function premiumEnabled() {
+  return String(process.env.KLONKT_PREMIUM_ENABLED || '').toLowerCase() === 'on';
+}
+export function licenseBase() { return LICENSE_URL; }
+
+// --- Publieke sleutel van de license-server cachen (voor offline verificatie) ---
+let _pubKey = null;
+async function licensePublicKey() {
+  if (_pubKey) return _pubKey;
+  const res = await fetch(`${LICENSE_URL}/pubkey`);
+  if (!res.ok) throw new Error('pubkey fetch faalde: ' + res.status);
+  const pem = await res.text();
+  _pubKey = crypto.createPublicKey(pem); // SPKI-PEM -> Ed25519 public key
+  return _pubKey;
+}
+
+function b64urlToBuf(s) {
+  return Buffer.from(String(s).replace(/-/g, '+').replace(/_/g, '/'), 'base64');
+}
+
+// Verifieer een entitlement-token (EdDSA-JWT van de license-server). Gooit bij
+// ongeldige handtekening/issuer/verlooptijd. Geeft de claims terug.
+export async function verifyEntitlementToken(token) {
+  const parts = String(token || '').split('.');
+  if (parts.length !== 3) throw new Error('malformed token');
+  const [h, p, s] = parts;
+  const header = JSON.parse(b64urlToBuf(h).toString('utf8'));
+  if (header.alg !== 'EdDSA') throw new Error('onverwacht alg');
+  const key = await licensePublicKey();
+  const ok = crypto.verify(null, Buffer.from(`${h}.${p}`), key, b64urlToBuf(s));
+  if (!ok) throw new Error('ongeldige handtekening');
+  const payload = JSON.parse(b64urlToBuf(p).toString('utf8'));
+  if (payload.iss !== ISSUER) throw new Error('onverwachte issuer');
+  if (payload.exp && payload.exp * 1000 < Date.now()) throw new Error('verlopen token');
+  return payload; // { sub, entitled, plan, lifetime_support_cents, exp, ... }
+}
+
+export function storeEntitlement(payload, token) {
+  setSetting('patreon_entitled', payload.entitled ? '1' : '0');
+  setSetting('patreon_sub', String(payload.sub || ''));
+  setSetting('patreon_support_cents', String(payload.lifetime_support_cents || 0));
+  setSetting('patreon_token_exp', String(payload.exp || 0));
+  setSetting('patreon_token', token || '');
+}
+
+export function clearEntitlement() {
+  for (const k of ['patreon_entitled', 'patreon_sub', 'patreon_support_cents', 'patreon_token_exp', 'patreon_token']) {
+    setSetting(k, '');
+  }
+}
+
+// Is deze instance premium? Premium-laag aan + een geldig, niet-verlopen,
+// entitled opgeslagen token. Patreon-lifetime daalt nooit, dus opnieuw koppelen
+// na verloop slaagt altijd.
+export function isPremium() {
+  if (!premiumEnabled()) return false;
+  if (getSetting('patreon_entitled') !== '1') return false;
+  const exp = Number(getSetting('patreon_token_exp', '0')) || 0;
+  if (exp && exp * 1000 < Date.now()) return false;
+  return true;
+}
+
+export function entitlementStatus() {
+  return {
+    enabled: premiumEnabled(),
+    premium: isPremium(),
+    connected: getSetting('patreon_entitled') === '1',
+    sub: getSetting('patreon_sub', '') || null,
+    supportCents: Number(getSetting('patreon_support_cents', '0')) || 0,
+    exp: Number(getSetting('patreon_token_exp', '0')) || 0,
+  };
+}
Index: src/views/pages/admin-settings.ejs
===================================================================
--- src/views/pages/admin-settings.ejs	(revision a9e23f60c1eb494375ea79b53c1bdc9914deae61)
+++ src/views/pages/admin-settings.ejs	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
@@ -4,4 +4,5 @@
 
   <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
+  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
 
   <section class="set-card">
@@ -37,4 +38,26 @@
     </form>
   </section>
+
+  <%# Premium (Patreon) — alleen zichtbaar als de self-hoster de premium-laag aanzet
+      (env KLONKT_PREMIUM_ENABLED=on). Standaard onzichtbaar; niets gegate. %>
+  <% if (premium && premium.enabled) { %>
+  <section class="set-card" style="margin-top:1rem">
+    <h2>Premium (Patreon)</h2>
+    <p class="set-help">Ontgrendel de premium-modules (Hub-modus, Statistieken, Fan-login)
+      met je <strong>$10-lifetime</strong> Patreon-steun. De app en alle updates blijven gratis.</p>
+    <% if (premium.premium) { %>
+      <p class="set-help"><strong>✓ Premium actief.</strong>
+        <% if (premium.supportCents) { %>Lifetime-steun: $<%= (premium.supportCents/100).toFixed(2) %>.<% } %></p>
+      <a href="/admin/patreon/disconnect" class="btn">Patreon ontkoppelen</a>
+    <% } else if (premium.connected) { %>
+      <p class="set-help">Patreon gekoppeld, maar nog geen $10 lifetime<% if (premium.supportCents) { %>
+        (nu $<%= (premium.supportCents/100).toFixed(2) %>)<% } %>. Steun de campagne en koppel opnieuw.</p>
+      <a href="/admin/patreon/connect" class="btn btn-primary">Opnieuw koppelen</a>
+    <% } else { %>
+      <p class="set-help">Nog niet gekoppeld.</p>
+      <a href="/admin/patreon/connect" class="btn btn-primary">Koppel Patreon</a>
+    <% } %>
+  </section>
+  <% } %>
 
   <% if (tenancy === 'hub') { %>
