Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/config/database.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -38,14 +38,7 @@
   // SQLite throws if the column already exists; we swallow that.
   ensureColumn('sites', 'enable_audio_player', 'INTEGER DEFAULT 1');
-  // Guardian 2: losse guardians. Een guardian-only account is user + minimale
-  // site (alleen de actor telt); de vlag houdt CMS/listings erbuiten.
-  ensureColumn('sites', 'guardian_only', 'INTEGER DEFAULT 0');
-  db.exec(`CREATE TABLE IF NOT EXISTS ap_guardian_invites (
-    token TEXT PRIMARY KEY,
-    created_by TEXT NOT NULL,
-    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
-    used_by TEXT,
-    used_at TEXT
-  )`);
+  // (Verwijderd 31-7-2026: sites.guardian_only en ap_guardian_invites hoorden
+  // bij de guardian-lite accounts. Bestaande installaties houden kolom en tabel
+  // ongebruikt; nieuwe krijgen ze niet meer.)
   // FEP-633c §5.3: follows targeting a ward are held pending until its
   // guardians approve (Guardian 2). Gating applies only to ward-actors.
Index: src/middleware/render.js
===================================================================
--- src/middleware/render.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/middleware/render.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -200,6 +200,6 @@
     canManageFedi,
     apEnabled: apEnabled(),
-    // Cirkel = the artists you feature (auto-boost). Shown when AP is on and you
-    // auto-boost ≥1 account, or (legacy) on a circle-tenancy site.
+    // Cirkel = the artists you feature (auto-boost): shown when AP is on and
+    // you auto-boost at least one account.
     hasCirkel: !!(_site && apEnabled() && (ActivityPubService.autoBoostCount(_site.slug) > 0 || ActivityPubService.boostedCount(_site.slug) > 0)),
     isViewer: _isViewer,
@@ -213,6 +213,4 @@
     audioTracks: data.audioTracks || res.locals.audioTracks || [],
     siteUrlBase: res.locals.siteUrlBase || '',
-    tenancy: res.locals.tenancy || 'solo',
-    hubTitle: getSetting('hub_title') || '',
     footerNewsletter: getSetting('footer_newsletter') === '1', // newsletter sign-up in footer (premium)
     agendaEnabled: getSetting('agenda_enabled') === '1', // show agenda/events in the pill (premium, opt-in)
Index: src/middleware/site.js
===================================================================
--- src/middleware/site.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/middleware/site.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -10,5 +10,4 @@
 
 import db from '../config/database.js';
-import { getTenancy } from '../services/SettingsService.js';
 import { audioUrl } from '../services/AudioStreamService.js';
 import { audioEnabled } from '../config/features.js';
@@ -27,32 +26,7 @@
 
 export function resolveSite(req, res, next) {
-  const tenancy = getTenancy();
-  res.locals.tenancy = tenancy; // also available in views
-
-  // In HUB mode /user/:slug maps to a specific site. In SOLO mode there is only
-  // one site: we skip that routing and pin to the primary site.
-  if (tenancy === 'hub') {
-    // A Klonkt site is canonically reachable via /user/:slug. /sites/:slug is a
-    // legacy alias → 301 to the canonical form so one URL scheme remains
-    // (preserves path + query string; does NOT touch /admin/sites, which starts with /admin/).
-    const m = req.path.match(/^\/(sites|user)\/([a-zA-Z0-9_-]+)(\/.*)?$/);
-    if (m) {
-      if (m[1] === 'sites') {
-        return res.redirect(301, req.originalUrl.replace(/^\/sites\//, '/user/'));
-      }
-      const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(m[2]);
-      if (site) {
-        res.locals.site = site;
-        req.url = (m[3] || '/'); // strip /user/:slug zodat downstream de rest ziet
-        res.locals.siteUrlBase = `/user/${m[2]}`;
-        return next();
-      }
-    }
-    // (Removed: a dead "slug == hostname" subdomain hack. Slugs may not contain
-    // dots, so it could never match. Real subdomain routing would match the
-    // subdomain LABEL against the slug — a separate feature, not this.)
-  }
-
-  // Solo (or hub without a match): pin to the primary/main site.
+  // One instance is one owner (Robins besluit, 31-7): there is one site tree,
+  // pinned to the primary site. The /user/:slug routing that hub mode needed
+  // is gone with it.
   const defaultSite = getPrimarySite();
   if (defaultSite) {
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({
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/services/ActivityPubService.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -26,5 +26,4 @@
 import EmbedResolver from './EmbedResolver.js';
 import Push from './PushService.js';
-import { getTenancy } from './SettingsService.js';
 import { t as i18nT } from './i18n.js';
 import Blocklist from './BlocklistService.js';
@@ -1418,8 +1417,8 @@
   for (const cb of cbs) { try { cb(); } catch { /* a waiter must never break the rest */ } }
 }
-// Hub-aware path prefix for a site's pages ('' in solo).
-function pushPrefix(slug) {
-  try { return getTenancy() === 'hub' ? `/user/${slug}` : ''; } catch { return ''; }
-}
+// Path prefix for a site's pages. One instance is one owner, so the site
+// lives at the root; kept as a function because the push URLs read like
+// `${pushPrefix(slug)}/messages` all over this file.
+function pushPrefix() { return ''; }
 // Notification language: the site's content language (fallback: instance default).
 function pushLang(slug) {
Index: src/services/SettingsService.js
===================================================================
--- src/services/SettingsService.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/services/SettingsService.js	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -1,11 +1,7 @@
-// Global app settings (key/value, cached). Primarily used for the tenancy mode.
+// Global app settings (key/value, cached).
 //
-//   tenancy = 'solo'   -> exactly one site (the primary/owner site)
-//   tenancy = 'circle' -> solo site that federates with other solo Klonkt sites
-//
-// HUB MODE IS REMOVED (2026-06-24): multi-artist-per-domain was dropped in favour
-// of solo + Cirkels. getTenancy() coerces any legacy 'hub' value to 'solo' so all
-// the old `tenancy === 'hub'` branches are unreachable; the hub code is being
-// deleted incrementally.
+// One instance is one owner (Robins besluit, 31-7-2026). The old tenancy modes
+// (hub = many artists on one domain, circle) are gone, code and all: the
+// branches were already unreachable and have now been deleted.
 //
 // The cache is updated immediately on setSetting, so a toggle in admin
@@ -39,14 +35,4 @@
 }
 
-export function getTenancy() {
-  // Tenancy is retired: 'hub' and 'circle' were both removed. Every site is
-  // 'solo'. Cirkels are now an ActivityPub feature (auto-boost), not a mode.
-  return 'solo';
-}
-
-export function setTenancy() {
-  setSetting('tenancy', 'solo');
-}
-
 // ActivityPub / fediverse federation. ON by default. '0' = off: the site does
 // not federate, /ap/* is gone, and the "from the fediverse" reactions disappear
Index: src/views/pages/admin-site-edit.ejs
===================================================================
--- src/views/pages/admin-site-edit.ejs	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/views/pages/admin-site-edit.ejs	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -14,5 +14,5 @@
         <span class="slug-url">
           <span class="slug-url-host" id="slug-host"
-                data-prefix="<%= (typeof tenancy !== 'undefined' && tenancy === 'hub') ? '/user/' : '/' %>">website.com/</span><input
+                data-prefix="/">website.com/</span><input
                 type="text" name="slug" id="slug-input" class="slug-url-input" value="<%= site.slug %>" required <% if (!isNew) { %>readonly<% } %>
                 pattern="[a-z0-9_-]{2,40}" placeholder="<%= t('asite.slug_placeholder') %>"
@@ -24,18 +24,4 @@
         <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
       </label>
-      <%# Assign (or reassign) owner — ONLY god AND only in hub mode. In hub this
-          gives each user their own self-managed Klonkt. In solo (one site,
-          one owner) this is pointless/confusing, so it's hidden. %>
-      <% if (user && user.role === 'god' && (typeof tenancy !== 'undefined' && tenancy === 'hub')) { %>
-      <label>
-        <span><%= t('asite.owner') %> <small class="form-hint-inline"><%= t('asite.owner_hint') %></small></span>
-        <% var _godSuffix = t('asite.owner_god_suffix'); %>
-        <select name="owner_id">
-          <% (typeof users !== 'undefined' ? users : []).forEach(function(u){ %>
-            <option value="<%= u.id %>" <%= (site.owner_id === u.id) ? 'selected' : '' %>><%= u.username %><%= u.role === 'god' ? _godSuffix : '' %></option>
-          <% }); %>
-        </select>
-      </label>
-      <% } %>
       <label>
         <span><%= t('asite.tagline') %> <small class="form-hint-inline"><%= t('asite.tagline_hint') %></small></span>
Index: src/views/pages/admin-users.ejs
===================================================================
--- src/views/pages/admin-users.ejs	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/views/pages/admin-users.ejs	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -71,10 +71,4 @@
 
           <div class="ax-user-controls">
-            <%# Hub: give this user their own self-managed Klonkt — opens the
-                new-site form with this user already pre-selected as owner. %>
-            <% if (typeof tenancy !== 'undefined' && tenancy === 'hub' && canMutate) { %>
-              <a href="/admin/sites/new?owner=<%= u.id %>" class="ax-icon-btn"
-                 aria-label="<%= t('ausr.new_klonkt_for', { name: u.username }) %>" title="<%= _u_newklonkt %>">＋🌐</a>
-            <% } %>
             <form method="post" action="/admin/users/<%= u.id %>/role" class="ax-role-form">
               <label class="ax-role">
Index: src/views/pages/admin.ejs
===================================================================
--- src/views/pages/admin.ejs	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/views/pages/admin.ejs	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -17,7 +17,5 @@
     .prem-badge:hover{filter:brightness(1.08)}
   </style>
-  <% if (tenancy === 'hub') { %>
-    <p class="admin-tagline"><%= t('admin.tagline_hub') %></p>
-  <% } else if (typeof circlesOn !== 'undefined' && circlesOn) { %>
+  <% if (typeof circlesOn !== 'undefined' && circlesOn) { %>
     <p class="admin-tagline"><%= t('admin.tagline_cirkels') %></p>
   <% } else { %>
@@ -26,14 +24,4 @@
 
   <nav class="admin-quick-links" aria-label="<%= t('nav.admin') %>">
-    <% if (tenancy === 'hub') { %>
-      <a href="/admin/sites" class="btn"><%= t('admin.b_sites') %></a>
-      <a href="/admin/users" class="btn"><%= t('admin.b_users') %></a>
-      <a href="/admin/media" class="btn"><%= t('admin.b_media') %></a>
-      <% if (typeof audioEnabled === 'undefined' || audioEnabled) { %>
-      <a href="/admin/playlists" class="btn"><%= t('admin.b_playlists') %></a>
-      <% } %>
-      <% if (primarySite) { %><a href="/admin/seo" class="btn"><%= t('admin.b_seo') %></a><% } %>
-      <a href="/admin/settings" class="btn"><%= t('admin.b_settings') %></a>
-    <% } else { %>
       <a href="/posts/new" class="btn"><%= t('admin.b_newpost') %></a>
       <a href="/admin/media" class="btn"><%= t('admin.b_media') %></a>
@@ -45,12 +33,11 @@
       <% if (primarySite) { %><a href="/admin/seo" class="btn"><%= t('admin.b_seo') %></a><% } %>
       <a href="/admin/settings" class="btn"><%= t('admin.b_settings') %></a>
-    <% } %>
     <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
       <a href="/admin/stats" class="btn"><%= t('admin.b_stats') %></a>
       <a href="/admin/paid" class="btn"><%= t('admin.b_paid') %></a>
       <a href="/admin/newsletter" class="btn"><%= t('admin.b_newsletter') %></a>
-      <% if (tenancy !== 'hub' && primarySite) { %><a href="/pers" class="btn" target="_blank"><%= t('admin.b_perskit') %></a><% } %>
-      <% if (tenancy !== 'hub' && primarySite) { %><a href="/downloads" class="btn" target="_blank"><%= t('admin.b_downloads') %></a><% } %>
-      <% if (tenancy !== 'hub' && primarySite) { %><a href="/links" class="btn" target="_blank"><%= t('admin.b_linkbio') %></a><% } %>
+      <% if (primarySite) { %><a href="/pers" class="btn" target="_blank"><%= t('admin.b_perskit') %></a><% } %>
+      <% if (primarySite) { %><a href="/downloads" class="btn" target="_blank"><%= t('admin.b_downloads') %></a><% } %>
+      <% if (primarySite) { %><a href="/links" class="btn" target="_blank"><%= t('admin.b_linkbio') %></a><% } %>
       <a href="/admin/shows" class="btn"><%= t('admin.b_agenda') %></a>
     <% } %>
@@ -63,8 +50,4 @@
 
   <div class="admin-stats">
-    <% if (tenancy === 'hub') { %>
-      <div class="stat-card"><div class="stat-num"><%= stats.users %></div><div class="stat-label"><%= t('admin.st_users') %></div></div>
-      <div class="stat-card"><div class="stat-num"><%= stats.sites %></div><div class="stat-label"><%= t('admin.st_sites') %></div></div>
-    <% } %>
     <div class="stat-card"><div class="stat-num"><%= stats.posts %></div><div class="stat-label"><%= t('admin.st_posts') %></div></div>
     <div class="stat-card"><div class="stat-num"><%= stats.published %></div><div class="stat-label"><%= t('admin.st_published') %></div></div>
@@ -90,42 +73,4 @@
   <% } %>
 
-  <% if (sites && sites.length) { %>
-    <h2><%= t('admin.sec_sites') %></h2>
-    <table class="admin-table">
-      <thead>
-        <tr><th><%= t('admin.th_slug') %></th><th><%= t('admin.th_title') %></th><th><%= t('admin.th_owner') %></th><th><%= t('admin.th_created') %></th></tr>
-      </thead>
-      <tbody>
-        <% sites.forEach(function(s) { %>
-          <tr>
-            <td><code><%= s.slug %></code></td>
-            <td><%= s.title %></td>
-            <td><%= s.owner_username || '—' %></td>
-            <td><%= formatDate(s.created_at) %></td>
-          </tr>
-        <% }); %>
-      </tbody>
-    </table>
-  <% } %>
-
-  <% if (users && users.length) { %>
-    <h2><%= t('admin.sec_users') %></h2>
-    <table class="admin-table">
-      <thead>
-        <tr><th><%= t('admin.th_username') %></th><th><%= t('admin.th_email') %></th><th><%= t('admin.th_role') %></th><th><%= t('admin.th_joined') %></th></tr>
-      </thead>
-      <tbody>
-        <% users.forEach(function(u) { %>
-          <tr>
-            <td><%= u.username %></td>
-            <td><%= u.email %></td>
-            <td><span class="role-pill role-<%= u.role %>"><%= u.role %></span></td>
-            <td><%= formatDate(u.created_at) %></td>
-          </tr>
-        <% }); %>
-      </tbody>
-    </table>
-  <% } %>
-
 </div>
 
Index: src/views/pages/guardian.ejs
===================================================================
--- src/views/pages/guardian.ejs	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/views/pages/guardian.ejs	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -26,7 +26,4 @@
     <span class="g-me" title="<%= t('guardian.acting_as') %>">@<%= state.site %></span>
     <% } %>
-    <form method="post" action="/guardian/invite" style="display:inline">
-    <button class="quiet small" type="submit">Invite a guardian</button>
-  </form>
 </header>
 
Index: src/views/partials/bottom-tab.ejs
===================================================================
--- src/views/partials/bottom-tab.ejs	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/views/partials/bottom-tab.ejs	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -13,8 +13,5 @@
                         && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site));
 const _ownProfile  = user ? ('/users/' + user.username) : null;
-const _isHub       = (typeof tenancy !== 'undefined' && tenancy === 'hub');
-// Home in hub mode ALWAYS goes to the hub root (/), not to an artist's sub-home.
-// In solo/circle _siteUrlBase is empty, so it stays '/'.
-const _homeHref    = (_isHub ? '' : _siteUrlBase) + '/';
+const _homeHref    = _siteUrlBase + '/';
 
 // Strip site prefix for cleaner matching
@@ -26,6 +23,5 @@
 
 let _active = null;
-// In hub, Home is only active on the real hub root, not on an artist sub-home.
-if (_isHub ? (_path === '/') : (_p === '/' || _p === '')) _active = 'home';
+if (_p === '/' || _p === '') _active = 'home';
 else if (_p.indexOf('/search') === 0 || _p.indexOf('/tag/') === 0 || _p.indexOf('/type/') === 0) _active = 'search';
 else if (_p === '/posts/new' || /^\/posts\/[^/]+\/edit$/.test(_p)) _active = 'create';
Index: src/views/partials/chrome.ejs
===================================================================
--- src/views/partials/chrome.ejs	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/views/partials/chrome.ejs	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -17,6 +17,5 @@
 var _isAuth    = typeof bodyClass === 'string' && bodyClass.indexOf('on-auth') >= 0;
 var _isChat    = typeof bodyClass === 'string' && bodyClass.indexOf('on-chat') >= 0;
-var hubLanding = (typeof bodyClass === 'string' && bodyClass.indexOf('on-hub') >= 0)
-   || ((typeof tenancy !== 'undefined' && tenancy === 'hub') && !siteUrlBase && !_isAdmin);
+var hubLanding = (typeof bodyClass === 'string' && bodyClass.indexOf('on-hub') >= 0);
 // Zelfstandige premium-feature-pagina's hebben hun eigen kop → geen profielkop/
 // switcher, wél de topnav (Robin 2026-06-18: "bovenste weg, behoud de nav").
Index: src/views/partials/topnav.ejs
===================================================================
--- src/views/partials/topnav.ejs	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
+++ src/views/partials/topnav.ejs	(revision 72ec6a4c535a846688e8da900f308a46345d1578)
@@ -28,16 +28,4 @@
       <% if (_isAdmin || _isSpecial) { %>
         <a href="<%= _siteUrlBase %>/" class="nav-link nav-link-admin"><%= t('nav.back_to_site') %></a>
-      <% } else if (typeof tenancy !== 'undefined' && tenancy === 'hub') { %>
-        <%# Hub: the brand navigates to the hub home via HTMX. The chrome is always
-            rendered in hub mode and hidden on the landing via body.on-hub (CSS), so htmx
-            can switch without a full reload → the audio player stays alive. %>
-        <a class="nav-hub-home" href="/" aria-label="Home" title="Home"
-           hx-get="/?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/" hx-indicator="#pcms-loading">
-          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
-        </a>
-      <% } else if (site) { %>
-        <a class="site-title" href="<%= _siteUrlBase %>/"
-           hx-get="<%= _siteUrlBase %>/?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
-           hx-push-url="<%= _siteUrlBase %>/" hx-indicator="#pcms-loading"><%= site.title %></a>
       <% } else { %>
         <a class="site-title" href="/">Klonkt</a>
