Index: src/routes/admin-settings.js
===================================================================
--- src/routes/admin-settings.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/routes/admin-settings.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -1,5 +1,4 @@
 /**
  * Admin: global settings.
- *  - tenancy mode (Solo/Hub)
  *  - hub branding (name/tagline/intro/hero of the generic hub home page)
  *
@@ -20,5 +19,5 @@
 import { renderPage } from '../middleware/render.js';
 import { requireGod } from '../middleware/auth.js';
-import { getTenancy, setTenancy, getSetting, setSetting } from '../services/SettingsService.js';
+import { getSetting, setSetting } from '../services/SettingsService.js';
 import { SUPPORTED } from '../services/i18n.js';
 import { mailerStatus, sendMail } from '../config/mailer.js';
@@ -69,6 +68,4 @@
     pageTitleKey: 'admin.t_settings',
     bodyClass: 'on-admin',
-    tenancy: getTenancy(),
-    hubTitle: getSetting('hub_title') || '',
     hubTagline: getSetting('hub_tagline') || '',
     hubIntro: getSetting('hub_intro') || '',
@@ -87,5 +84,5 @@
 router.post('/', requireGod, (req, res) => {
   // multer.single processes multipart (hub branding form). For a plain
-  // urlencoded POST (tenancy form) multer does nothing and req.body stays intact.
+  // urlencoded POST: multer does nothing and req.body stays intact.
   heroUpload.single('hub_hero_file')(req, res, (err) => {
     if (err) {
@@ -93,8 +90,4 @@
     }
 
-    if (typeof req.body.tenancy !== 'undefined') {
-      // Hub mode is removed → setTenancy only accepts solo | circle (coerces the rest).
-      setTenancy(req.body.tenancy);
-    }
     if (typeof req.body.default_lang !== 'undefined') {
       // Default language for visitors (empty = follow env/browser). Validated against NL/EN/DE.
@@ -109,7 +102,4 @@
       if (tz) { try { Intl.DateTimeFormat('en-US', { timeZone: tz }); valid = tz; } catch { valid = ''; } }
       setSetting('timezone', valid);
-    }
-    if (typeof req.body.hub_title !== 'undefined') {
-      setSetting('hub_title', (req.body.hub_title || '').toString().slice(0, 80).trim());
     }
     if (typeof req.body.hub_tagline !== 'undefined') {
Index: src/routes/admin.js
===================================================================
--- src/routes/admin.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/routes/admin.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -9,5 +9,5 @@
 import { renderPage } from '../middleware/render.js';
 import { requireAuth } from '../middleware/auth.js';
-import { getTenancy, apEnabled } from '../services/SettingsService.js';
+import { apEnabled } from '../services/SettingsService.js';
 import { getPrimarySite } from '../middleware/site.js';
 
@@ -17,6 +17,6 @@
 // Solves the problem that drafts (status != published) were not findable anywhere:
 // the timeline shows only published posts.
-function sitePosts(siteId, siteSlug, tenancy, limit = 60) {
-  const base = tenancy === 'hub' ? `/user/${siteSlug}` : '';
+function sitePosts(siteId, siteSlug, limit = 60) {
+  const base = '';
   return db.prepare(`
     SELECT slug, title, status, published_at, created_at, updated_at
@@ -53,12 +53,10 @@
       mySite,
       mine,
-      posts: sitePosts(mySite.id, mySite.slug, 'hub'), // my-site is hub-only
+      posts: sitePosts(mySite.id, mySite.slug),
     });
   }
 
-  const tenancy = getTenancy();
-
-  // The primary/main site — in solo THE site, in hub the main site. Provides the
-  // "Appearance" tile with its edit link + the posts/drafts list.
+  // THE site: it provides the "Appearance" tile with its edit link and the
+  // posts/drafts list.
   const primarySite = getPrimarySite();
 
@@ -72,34 +70,14 @@
   };
 
-  // Sites/users tables are only relevant in hub mode; in solo we skip the query.
-  const sites = tenancy === 'hub' ? db.prepare(`
-    SELECT s.slug, s.title, s.created_at, u.username AS owner_username
-    FROM sites s
-    LEFT JOIN users u ON u.id = s.owner_id
-    ORDER BY s.created_at DESC
-    LIMIT 50
-  `).all() : [];
-
-  const users = tenancy === 'hub' ? db.prepare(`
-    SELECT username, email, role, created_at
-    FROM users
-    ORDER BY created_at DESC
-    LIMIT 50
-  `).all() : [];
-
-  // Posts/drafts of the primary site (in solo = the site; in hub = the admin's
-  // main site). Drafts are listed first so they are easy to find.
-  const posts = primarySite ? sitePosts(primarySite.id, primarySite.slug, tenancy) : [];
+  // Posts/drafts of the site. Drafts are listed first so they are easy to find.
+  const posts = primarySite ? sitePosts(primarySite.id, primarySite.slug) : [];
 
   renderPage(req, res, 'pages/admin', {
     pageTitleKey: 'admin.t_admin',
     bodyClass: 'on-admin',
-    tenancy,
-    circlesOn: apEnabled(),   // solo vs cirkels tagline (federatie aan/uit)
+    circlesOn: apEnabled(),   // federatie aan/uit in de tagline
     primarySite,
     stats,
-    sites,
     posts,
-    users,
   });
 });
@@ -111,5 +89,4 @@
     pageTitleKey: 'admin.t_manual',
     bodyClass: 'on-admin',
-    tenancy: getTenancy(),
   });
 });
Index: src/routes/audio.js
===================================================================
--- src/routes/audio.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/routes/audio.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -156,5 +156,4 @@
   const id = String(req.params.id || '');
   if (!/^[A-Za-z0-9_-]+$/.test(id)) return res.status(400).json({ error: 'bad id' });
-  const isHub = res.locals.tenancy === 'hub';
   const row = db.prepare(`
     SELECT p.slug, s.slug AS site_slug
@@ -164,5 +163,5 @@
   `).get('%[[track:' + id + ']]%');
   if (!row) return res.status(404).json({ error: 'not found' });
-  const url = isHub ? `/user/${row.site_slug}/${row.slug}` : `/${row.slug}`;
+  const url = `/${row.slug}`;
   res.json({ url });
 });
Index: src/routes/download.js
===================================================================
--- src/routes/download.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/routes/download.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -34,5 +34,5 @@
   ).get(site.id);
   if (!post) return {};
-  try { return postNeighbors(site, post, res.locals.tenancy === 'hub'); } catch (e) { return {}; }
+  try { return postNeighbors(site, post); } catch (e) { return {}; }
 }
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/routes/guardian.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -10,6 +10,4 @@
  */
 import express from 'express';
-import crypto from 'crypto';
-import bcrypt from 'bcryptjs';
 import path from 'path';
 import { fileURLToPath } from 'url';
@@ -595,65 +593,10 @@
 });
 
-// ── Losse guardians (Guardian 2): uitnodigen en aansluiten ───────────────
-// De familie nodigt oma uit; zij kiest naam + wachtwoord en heeft daarmee een
-// guardian-only account: user + minimale site (guardian_only=1). Alles wat al
-// per slug werkt (actor, inbox, offers, push, deze PWA) werkt dan meteen.
-
-router.post('/invite', requireAuth, (req, res) => {
-  const token = crypto.randomBytes(16).toString('base64url');
-  db.prepare('INSERT INTO ap_guardian_invites (token, created_by) VALUES (?,?)')
-    .run(token, req.session.user.id);
-  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
-  const url = `${base}/guardian/join/${token}`;
-  res.send(`<!doctype html><meta charset="utf-8"><body style="font-family:sans-serif;max-width:480px;margin:40px auto">
-    <h2>Invite a guardian</h2>
-    <p>Share this link. It lets one person create a guardian account here:</p>
-    <p><a href="${url}">${url}</a></p>
-    <p><a href="/guardian">Back</a></p></body>`);
-});
-
-function joinForm(token, error) {
-  return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
-  <body style="font-family:sans-serif;max-width:420px;margin:40px auto">
-  <h2>Become a guardian</h2>
-  <p>Watch over someone you care about. Pick a name and a password; that is all.</p>
-  ${error ? `<p style="color:#b00">${error}</p>` : ''}
-  <form method="post" action="/guardian/join/${token}">
-    <p><input name="name" placeholder="your name (grandma)" required pattern="[a-z0-9_-]{1,32}"
-       style="width:100%;padding:10px" autocapitalize="none"></p>
-    <p><input name="password" type="password" placeholder="password" required minlength="8"
-       style="width:100%;padding:10px"></p>
-    <p><button style="width:100%;padding:12px">Create my guardian account</button></p>
-  </form></body>`;
-}
-
-router.get('/join/:token', (req, res) => {
-  const inv = db.prepare('SELECT * FROM ap_guardian_invites WHERE token = ? AND used_at IS NULL')
-    .get(req.params.token);
-  if (!inv) return res.status(404).send('This invite is no longer valid.');
-  res.send(joinForm(req.params.token));
-});
-
-router.post('/join/:token', express.urlencoded({ extended: false }), (req, res) => {
-  const inv = db.prepare('SELECT * FROM ap_guardian_invites WHERE token = ? AND used_at IS NULL')
-    .get(req.params.token);
-  if (!inv) return res.status(404).send('This invite is no longer valid.');
-  const name = String(req.body.name || '').trim().toLowerCase();
-  const password = String(req.body.password || '');
-  if (!/^[a-z0-9_-]{1,32}$/.test(name)) return res.status(400).send(joinForm(req.params.token, 'Only lowercase letters, digits, - and _.'));
-  if (password.length < 8) return res.status(400).send(joinForm(req.params.token, 'Password: at least 8 characters.'));
-  if (db.prepare('SELECT 1 FROM sites WHERE slug = ?').get(name) || db.prepare('SELECT 1 FROM users WHERE username = ?').get(name)) {
-    return res.status(409).send(joinForm(req.params.token, 'That name is taken, pick another.'));
-  }
-  const userId = crypto.randomUUID();
-  db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
-    .run(userId, name, `${name}@guardian.invalid`, bcrypt.hashSync(password, 10), 'member');
-  db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary, guardian_only) VALUES (?,?,?,?,0,1)')
-    .run(crypto.randomUUID(), name, name, userId);
-  db.prepare('UPDATE ap_guardian_invites SET used_by = ?, used_at = CURRENT_TIMESTAMP WHERE token = ?')
-    .run(userId, req.params.token);
-  req.session.user = { id: userId, username: name, role: 'member' };
-  res.redirect('/guardian');
-});
+// Losse guardian-accounts (guardian-lite: /invite + /join, user + site met
+// guardian_only=1) zijn verwijderd op 31-7-2026. Een instance is een eigenaar;
+// zo'n account was de laatste multi-user-rest en zette bovendien andermans
+// wachtwoordhash, sessie en PRIVATE actor-sleutel in jouw database, wat een
+// verhuizing (shaer-qw6q) onmogelijk netjes maakte. Een guardian hoort een
+// eigen Klonkt te hebben; de adoptie loopt dan gewoon over de federatie.
 
 export default router;
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/routes/posts.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -763,23 +763,15 @@
 }
 
-function postNeighbors(site, post, isHub) {
-  const urlBaseFor = (p) => (isHub && p && p.site_slug) ? `/user/${p.site_slug}` : '';
-  const ordered = isHub
-    ? db.prepare(`
-        SELECT p.id, p.slug, p.title, p.pinned, s.slug AS site_slug
-        FROM posts p JOIN sites s ON s.id = p.site_id
-        WHERE p.status = 'published'
-        ORDER BY p.published_at DESC
-      `).all()
-    : db.prepare(`
-        SELECT id, slug, title, pinned FROM posts
-        WHERE site_id = ? AND status = 'published'
-        ORDER BY (pinned = 0) ASC, pinned ASC, published_at DESC
-      `).all(site.id);
+function postNeighbors(site, post) {
+  const ordered = db.prepare(`
+    SELECT id, slug, title, pinned FROM posts
+    WHERE site_id = ? AND status = 'published'
+    ORDER BY (pinned = 0) ASC, pinned ASC, published_at DESC
+  `).all(site.id);
   const idx = ordered.findIndex((p) => p.id === post.id);
   const newerPost = idx > 0 ? ordered[idx - 1] : null;
   const olderPost = (idx >= 0 && idx < ordered.length - 1) ? ordered[idx + 1] : null;
-  if (newerPost) newerPost._urlBase = urlBaseFor(newerPost);
-  if (olderPost) olderPost._urlBase = urlBaseFor(olderPost);
+  if (newerPost) newerPost._urlBase = '';
+  if (olderPost) olderPost._urlBase = '';
   return { newerPost, olderPost };
 }
@@ -1344,5 +1336,5 @@
   const _unlocked = _u && _u.purpose === 'unlocked' && _u.siteId === site.id && String(_u.post) === String(post.slug);
   if (post.paid && !canEditThis && !_unlocked) {
-    const { newerPost, olderPost } = postNeighbors(site, post, res.locals.tenancy === 'hub');
+    const { newerPost, olderPost } = postNeighbors(site, post);
     return renderPage(req, res, 'pages/paid-gate', {
       pageTitle: post.title || 'Voor supporters',
@@ -1364,5 +1356,5 @@
     // Same Newer/Older navigation as on a normal post, so the visitor doesn't get
     // stuck on the fan gate but can keep browsing.
-    const { newerPost, olderPost } = postNeighbors(site, post, res.locals.tenancy === 'hub');
+    const { newerPost, olderPost } = postNeighbors(site, post);
     return renderPage(req, res, 'pages/fan-gate', {
       pageTitle: post.title || 'Alleen voor fans',
@@ -1396,29 +1388,18 @@
   // Prev / next chronological (kept for back-compat — "post-nav" feature
   // below the article still uses these as a simple linear navigation).
-  // Hub mode: Related posts + Newer/Older pull from ALL users (all sites),
-  // newest first. Solo mode: within the current site (old behaviour).
-  const isHub = res.locals.tenancy === 'hub';
-  // Per-post URL base: in hub a link points to /user/<site-slug>/<post-slug>.
-  const urlBaseFor = (p) => (isHub && p && p.site_slug) ? `/user/${p.site_slug}` : '';
+  const urlBaseFor = () => '';
 
   // Newer/Older across ALL posts (shared helper — also used by the fan gate).
-  const { newerPost, olderPost } = postNeighbors(site, post, isHub);
+  const { newerPost, olderPost } = postNeighbors(site, post);
 
   // ── Related posts: same-tag matching with recency fallback ─────
   // Fetch ~50 candidates, score by tag overlap, take top 3.
   // Excluding self via `id != ?`.
-  const candidates = isHub
-    ? db.prepare(`
-        SELECT p.id, p.slug, p.title, p.cover_image_url, p.cover_video_url, p.published_at, p.tags, p.nsfw, p.content_warning, s.slug AS site_slug
-        FROM posts p JOIN sites s ON s.id = p.site_id
-        WHERE p.status = 'published' AND p.id != ?
-        ORDER BY p.published_at DESC LIMIT 50
-      `).all(post.id)
-    : db.prepare(`
-        SELECT id, slug, title, cover_image_url, cover_video_url, published_at, tags, nsfw, content_warning
-        FROM posts
-        WHERE site_id = ? AND status = 'published' AND id != ?
-        ORDER BY published_at DESC LIMIT 50
-      `).all(site.id, post.id);
+  const candidates = db.prepare(`
+    SELECT id, slug, title, cover_image_url, cover_video_url, published_at, tags, nsfw, content_warning
+    FROM posts
+    WHERE site_id = ? AND status = 'published' AND id != ?
+    ORDER BY published_at DESC LIMIT 50
+  `).all(site.id, post.id);
 
   // Parse tags JSON safely; missing/malformed → empty array.
Index: src/routes/search.js
===================================================================
--- src/routes/search.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/routes/search.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -51,7 +51,6 @@
 function searchSite(req, res, rawQ, lim) {
   const site = res.locals.site;
-  const isHub = res.locals.tenancy === 'hub';
   const base = res.locals.siteUrlBase || '';
-  const urlFor = (slug) => (isHub ? `/user/${site.slug}/${slug}` : `/${slug}`);
+  const urlFor = (slug) => `/${slug}`;
   // Eigen vertaler (werkt ook in de JSON-route, waar res.locals.t niet bestaat).
   const lang = resolveLang(req);
@@ -172,6 +171,5 @@
   if (rawQ.length < 2) return res.json({ posts: [], tracks: [], events: [], pages: [] });
 
-  const isHub = res.locals.tenancy === 'hub';
-  const urlFor = (slug) => (isHub ? `/user/${site.slug}/${slug}` : `/${slug}`);
+  const urlFor = (slug) => `/${slug}`;
   const r = searchSite(req, res, rawQ, { posts: 5, tracks: 4, events: 3, pages: 4 });
   res.json({
