Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 25d4041f989e999fe4673b2c9ac3e03f874f5980)
+++ src/config/database.js	(revision 0091cb77d389e73218336782b310d2f000e2579a)
@@ -47,4 +47,6 @@
   // Per-site Prutter toggle: when off, DM endpoints/UI are hidden for that site.
   ensureColumn('sites', 'enable_prutter', 'INTEGER DEFAULT 1');
+  // Cirkels: mag deze site in cirkels van anderen verschijnen (surfacing opt-out).
+  ensureColumn('sites', 'allow_circle', 'INTEGER DEFAULT 1');
 
   // v9 audit additions —————————————————————————————————————————
Index: src/routes/admin-circle.js
===================================================================
--- src/routes/admin-circle.js	(revision 0091cb77d389e73218336782b310d2f000e2579a)
+++ src/routes/admin-circle.js	(revision 0091cb77d389e73218336782b310d2f000e2579a)
@@ -0,0 +1,106 @@
+/**
+ * Admin: Cirkel-beheer (god-only).
+ *   GET  /admin/circle           -> lijst van cirkel-links + status
+ *   POST /admin/circle/add       -> Klonkt-URL toevoegen
+ *   POST /admin/circle/:id/remove
+ *   POST /admin/circle/:id/sync  -> nu verversen (pull + verifieer)
+ *   POST /admin/circle/allow     -> toggle "mag in cirkels van anderen verschijnen"
+ *
+ * Zie docs/cirkels-v1-spec.md §5d.
+ */
+
+import express from 'express';
+import crypto from 'crypto';
+import { renderPage } from '../middleware/render.js';
+import { requireGod } from '../middleware/auth.js';
+import db from '../config/database.js';
+import { getTenancy } from '../services/SettingsService.js';
+import { syncOne } from '../services/CircleService.js';
+
+const router = express.Router();
+
+function primarySite() {
+  return db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get();
+}
+
+router.get('/', requireGod, (req, res) => {
+  const site = primarySite();
+  const links = site
+    ? db.prepare('SELECT * FROM circle_links WHERE local_site_id = ? ORDER BY added_at DESC').all(site.id)
+    : [];
+  const counts = {};
+  for (const l of links) {
+    counts[l.id] = l.remote_actor_id
+      ? db.prepare('SELECT COUNT(*) AS n FROM remote_posts WHERE actor_id = ?').get(l.remote_actor_id).n
+      : 0;
+  }
+  renderPage(req, res, 'pages/admin-circle', {
+    pageTitle: 'Cirkel',
+    bodyClass: 'on-admin',
+    tenancy: getTenancy(),
+    site,
+    links,
+    counts,
+    allowCircle: site ? site.allow_circle !== 0 : true,
+    success: req.query.success || null,
+    error: req.query.error || null,
+  });
+});
+
+router.post('/add', requireGod, (req, res) => {
+  const site = primarySite();
+  if (!site) return res.redirect('/admin/circle?error=' + encodeURIComponent('Geen site gevonden'));
+  const url = (req.body.remote_url || '').toString().trim().replace(/\/+$/, '');
+  if (!/^https:\/\/[^\s/]+(\/[^\s]*)?$/i.test(url)) {
+    return res.redirect('/admin/circle?error=' + encodeURIComponent('Voer een geldige https-URL in'));
+  }
+  const label = (req.body.label || '').toString().slice(0, 80).trim() || null;
+  try {
+    db.prepare("INSERT INTO circle_links (id, local_site_id, remote_url, label, status) VALUES (?, ?, ?, ?, 'active')")
+      .run(crypto.randomUUID(), site.id, url, label);
+  } catch (e) {
+    return res.redirect('/admin/circle?error=' + encodeURIComponent('Deze site staat al in je cirkel'));
+  }
+  res.redirect('/admin/circle?success=' + encodeURIComponent('Toegevoegd — klik "Verversen" om op te halen'));
+});
+
+router.post('/:id/remove', requireGod, (req, res) => {
+  const link = db.prepare('SELECT * FROM circle_links WHERE id = ?').get(req.params.id);
+  if (link) {
+    db.prepare('DELETE FROM circle_links WHERE id = ?').run(link.id);
+    // Gecachte content opruimen als geen andere link nog naar deze actor wijst.
+    if (link.remote_actor_id) {
+      const other = db.prepare('SELECT 1 FROM circle_links WHERE remote_actor_id = ? LIMIT 1').get(link.remote_actor_id);
+      if (!other) {
+        db.prepare('DELETE FROM remote_posts WHERE actor_id = ?').run(link.remote_actor_id);
+        db.prepare('DELETE FROM remote_actors WHERE id = ?').run(link.remote_actor_id);
+      }
+    }
+  }
+  res.redirect('/admin/circle?success=' + encodeURIComponent('Verwijderd'));
+});
+
+router.post('/:id/sync', requireGod, async (req, res) => {
+  const link = db.prepare('SELECT * FROM circle_links WHERE id = ?').get(req.params.id);
+  if (!link) return res.redirect('/admin/circle?error=' + encodeURIComponent('Niet gevonden'));
+  try {
+    const r = await syncOne(link);
+    res.redirect('/admin/circle?success=' + encodeURIComponent(`Bijgewerkt — ${r.items} posts opgehaald`));
+  } catch (e) {
+    const msg = String((e && e.message) || e).slice(0, 300);
+    db.prepare("UPDATE circle_links SET status='error', last_error=?, last_synced=CURRENT_TIMESTAMP WHERE id=?")
+      .run(msg, link.id);
+    res.redirect('/admin/circle?error=' + encodeURIComponent(msg));
+  }
+});
+
+router.post('/allow', requireGod, (req, res) => {
+  const site = primarySite();
+  if (site) {
+    const v = (req.body.allow_circle === 'on' || req.body.allow_circle === '1') ? 1 : 0;
+    db.prepare('UPDATE sites SET allow_circle = ? WHERE id = ?').run(v, site.id);
+  }
+  res.redirect('/admin/circle?success=' + encodeURIComponent('Opgeslagen'));
+});
+
+export default router;
Index: src/routes/admin-settings.js
===================================================================
--- src/routes/admin-settings.js	(revision 25d4041f989e999fe4673b2c9ac3e03f874f5980)
+++ src/routes/admin-settings.js	(revision 0091cb77d389e73218336782b310d2f000e2579a)
@@ -33,5 +33,5 @@
 router.post('/', requireGod, (req, res) => {
   if (typeof req.body.tenancy !== 'undefined') {
-    setTenancy(req.body.tenancy === 'hub' ? 'hub' : 'solo');
+    setTenancy(req.body.tenancy); // valideert naar solo | hub | circle
   }
   if (typeof req.body.hub_title !== 'undefined') {
Index: src/routes/circle.js
===================================================================
--- src/routes/circle.js	(revision 0091cb77d389e73218336782b310d2f000e2579a)
+++ src/routes/circle.js	(revision 0091cb77d389e73218336782b310d2f000e2579a)
@@ -0,0 +1,55 @@
+/**
+ * Publieke Cirkel-feed: /cirkel
+ * Toont de gecachte publieke posts uit je cirkel als statische kaarten.
+ * Alleen actief als tenancy === 'circle' (anders next() -> postsRoutes/404).
+ * Zie docs/cirkels-v1-spec.md §5c.
+ */
+
+import express from 'express';
+import { renderPage } from '../middleware/render.js';
+import db from '../config/database.js';
+import { getTenancy } from '../services/SettingsService.js';
+
+const router = express.Router();
+
+function safeUrl(u) {
+  return typeof u === 'string' && /^https?:\/\//i.test(u) ? u : null;
+}
+function safeJson(s) {
+  try { return s ? JSON.parse(s) : []; } catch { return []; }
+}
+
+router.get('/cirkel', (req, res, next) => {
+  if (getTenancy() !== 'circle') return next();
+
+  const rows = db.prepare(`
+    SELECT p.id, p.published, p.title, p.summary, p.url, p.media_json,
+           a.name AS actor_name, a.url AS actor_url, a.avatar AS actor_avatar
+    FROM remote_posts p
+    JOIN remote_actors a ON a.id = p.actor_id
+    ORDER BY COALESCE(p.published, p.fetched_at) DESC
+    LIMIT 100
+  `).all();
+
+  const posts = rows.map((r) => {
+    const media = safeJson(r.media_json)
+      .map((m) => ({ ...m, url: safeUrl(m.url) }))
+      .filter((m) => m.url);
+    const image = media.find((m) => m.type === 'image') || null;
+    return {
+      id: r.id,
+      title: r.title || '(zonder titel)',
+      summary: r.summary || '',
+      url: safeUrl(r.url),
+      published: r.published,
+      actorName: r.actor_name || 'Onbekend',
+      actorUrl: safeUrl(r.actor_url),
+      actorAvatar: safeUrl(r.actor_avatar),
+      image: image ? image.url : null,
+    };
+  });
+
+  renderPage(req, res, 'pages/circle-feed', { pageTitle: 'Cirkel', posts });
+});
+
+export default router;
Index: src/server.js
===================================================================
--- src/server.js	(revision 25d4041f989e999fe4673b2c9ac3e03f874f5980)
+++ src/server.js	(revision 0091cb77d389e73218336782b310d2f000e2579a)
@@ -45,4 +45,6 @@
 import federationRoutes from './routes/federation.js';
 import { startCircleSyncLoop } from './services/CircleService.js';
+import adminCircleRoutes from './routes/admin-circle.js';
+import circleRoutes from './routes/circle.js';
 
 if (!process.env.SESSION_SECRET) {
@@ -225,4 +227,5 @@
 app.use('/admin/comments', adminCommentsRoutes);
 app.use('/admin/settings', adminSettingsRoutes);
+app.use('/admin/circle', adminCircleRoutes);
 app.use('/admin', adminRoutes);
 app.use('/prutter', prutterRoutes);
@@ -238,4 +241,5 @@
 app.get('/artiesten', (req, res) => res.redirect(301, req.originalUrl.replace(/^\/artiesten/, '/leden'))); // oude URL -> /leden
 app.use('/', hubRoutes); // hub-overview op '/' (solo: next() -> postsRoutes)
+app.use('/', circleRoutes); // /cirkel-feed (solo/hub: next() -> postsRoutes)
 app.use('/', postsRoutes);
 
Index: src/services/CircleFederation.js
===================================================================
--- src/services/CircleFederation.js	(revision 25d4041f989e999fe4673b2c9ac3e03f874f5980)
+++ src/services/CircleFederation.js	(revision 0091cb77d389e73218336782b310d2f000e2579a)
@@ -74,5 +74,5 @@
 // aan is_public (aparte expliciete flag volgt in de Beheer-UX-stap).
 function allowsCircle(site) {
-  return !!site && site.is_public !== 0;
+  return !!site && site.is_public !== 0 && site.allow_circle !== 0;
 }
 
Index: src/views/pages/admin-circle.ejs
===================================================================
--- src/views/pages/admin-circle.ejs	(revision 0091cb77d389e73218336782b310d2f000e2579a)
+++ src/views/pages/admin-circle.ejs	(revision 0091cb77d389e73218336782b310d2f000e2579a)
@@ -0,0 +1,110 @@
+<div class="container admin-page">
+  <h1>Cirkel</h1>
+  <p><a href="/admin/settings" class="btn">&larr; Instellingen</a></p>
+
+  <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
+  <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
+
+  <% if (tenancy !== 'circle') { %>
+    <section class="set-card">
+      <p class="set-help">De modus staat niet op <strong>Cirkels</strong>. Zet 'm aan bij
+        <a href="/admin/settings">Instellingen</a> om je cirkel-feed te tonen op <code>/cirkel</code>.
+        Je kunt hieronder al wel bronnen klaarzetten.</p>
+    </section>
+  <% } %>
+
+  <section class="set-card" style="margin-top:1rem">
+    <h2>Mijn zichtbaarheid</h2>
+    <p class="set-help">Mag jouw site verschijnen in cirkels van anderen? (Toont je publieke
+      posts via <code>/.klonkt/outbox.json</code>.)</p>
+    <form method="post" action="/admin/circle/allow" class="set-form">
+      <label class="set-opt" style="align-items:center">
+        <input type="checkbox" name="allow_circle" value="on" <%= allowCircle ? 'checked' : '' %>>
+        <span><strong>Toon mijn site in cirkels van anderen</strong></span>
+      </label>
+      <button type="submit" class="btn btn-primary">Opslaan</button>
+    </form>
+  </section>
+
+  <section class="set-card" style="margin-top:1rem">
+    <h2>Klonkt-site toevoegen</h2>
+    <p class="set-help">Plak de basis-URL van een andere Klonkt-site. Asymmetrisch: jij toont
+      hen, los van of zij jou tonen.</p>
+    <form method="post" action="/admin/circle/add" class="set-form">
+      <label class="set-field">
+        <span>URL</span>
+        <input type="url" name="remote_url" placeholder="https://artiest.klonkt.com" required>
+      </label>
+      <label class="set-field">
+        <span>Label <small style="font-weight:400">— optioneel</small></span>
+        <input type="text" name="label" maxlength="80" placeholder="bijv. Joost Klein">
+      </label>
+      <button type="submit" class="btn btn-primary">Toevoegen</button>
+    </form>
+  </section>
+
+  <section class="set-card" style="margin-top:1rem">
+    <h2>In mijn cirkel (<%= links.length %>)</h2>
+    <% if (!links.length) { %>
+      <p class="set-help">Nog geen bronnen. Voeg er hierboven een toe.</p>
+    <% } else { %>
+      <ul class="circ-list">
+        <% links.forEach(function(l){ %>
+          <li class="circ-item">
+            <div class="circ-main">
+              <strong><%= l.label || l.remote_url %></strong>
+              <small><%= l.remote_url %></small>
+              <small>
+                <% if (l.status === 'active') { %>
+                  <span class="circ-badge ok">&#10003; actief</span>
+                  <%= counts[l.id] || 0 %> posts
+                  <% if (l.last_synced) { %>&middot; laatst: <%= l.last_synced %><% } %>
+                <% } else if (l.status === 'error') { %>
+                  <span class="circ-badge err">&#9888; fout</span>
+                  <%= l.last_error || '' %>
+                <% } else { %>
+                  <span class="circ-badge">&#9208; gepauzeerd</span>
+                <% } %>
+              </small>
+            </div>
+            <div class="circ-actions">
+              <form method="post" action="/admin/circle/<%= l.id %>/sync" style="display:inline">
+                <button type="submit" class="btn">Verversen</button>
+              </form>
+              <form method="post" action="/admin/circle/<%= l.id %>/remove" style="display:inline"
+                    onsubmit="return confirm('Verwijderen uit je cirkel?')">
+                <button type="submit" class="btn btn-danger">Verwijder</button>
+              </form>
+            </div>
+          </li>
+        <% }); %>
+      </ul>
+    <% } %>
+  </section>
+</div>
+
+<style>
+.admin-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
+.admin-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 .25rem; }
+.set-card { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 12px; padding: 1.25rem; }
+.set-card h2 { font-family: var(--font-display, serif); font-size: 1.25rem; margin: 0 0 .5rem; }
+.set-help { color: var(--ink-muted); font-size: .9rem; margin: 0 0 1rem; }
+.set-form { display: flex; flex-direction: column; gap: 1rem; }
+.set-opt { display: flex; gap: .75rem; align-items: flex-start; padding: .85rem 1rem; border: 1px solid var(--rule); border-radius: 8px; background: var(--paper); }
+.set-field { display: flex; flex-direction: column; gap: .3rem; }
+.set-field > span { font-size: .8rem; font-weight: 600; color: var(--ink-soft, var(--ink-muted)); }
+.set-field input { width: 100%; box-sizing: border-box; padding: .55rem .7rem; border: 1px solid var(--rule); border-radius: 6px; background: var(--paper); color: var(--ink); font: inherit; }
+.set-form .btn, .circ-actions .btn { align-self: flex-start; }
+.circ-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .6rem; }
+.circ-item { display: flex; justify-content: space-between; gap: 1rem; align-items: center; padding: .75rem 1rem; border: 1px solid var(--rule); border-radius: 8px; background: var(--paper); flex-wrap: wrap; }
+.circ-main { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
+.circ-main small { color: var(--ink-muted); font-size: .78rem; word-break: break-all; }
+.circ-actions { display: flex; gap: .4rem; flex-shrink: 0; }
+.circ-badge { font-size: .72rem; font-weight: 700; padding: .1rem .45rem; border-radius: 20px; background: var(--rule); }
+.circ-badge.ok { background: #d1fae5; color: #065f46; }
+.circ-badge.err { background: #fee2e2; color: #991b1b; }
+.btn-danger { color: #991b1b; border-color: #fca5a5; }
+.alert { padding: .75rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
+.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
+.alert-error { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
+</style>
Index: src/views/pages/admin-settings.ejs
===================================================================
--- src/views/pages/admin-settings.ejs	(revision 25d4041f989e999fe4673b2c9ac3e03f874f5980)
+++ src/views/pages/admin-settings.ejs	(revision 0091cb77d389e73218336782b310d2f000e2579a)
@@ -25,4 +25,11 @@
           <strong>Hub</strong> — bedrijfssite met <code>/user/</code>.
           <small>Jij (admin) wijst gebruikers een eigen Klonkt Hub toe. Gesloten cirkel: geen open registratie.</small>
+        </span>
+      </label>
+      <label class="set-opt">
+        <input type="radio" name="tenancy" value="circle" <%= tenancy === 'circle' ? 'checked' : '' %>>
+        <span>
+          <strong>Cirkels</strong> &mdash; solo + federatie.
+          <small>E&eacute;n eigen site die de publieke posts van andere Klonkt-sites toont. Asymmetrisch: jij bepaalt wie in jouw cirkel zit.</small>
         </span>
       </label>
@@ -59,4 +66,15 @@
   </section>
   <% } %>
+
+  <% if (tenancy === 'circle') { %>
+  <section class="set-card" style="margin-top:1rem">
+    <h2>Cirkel</h2>
+    <p class="set-help">
+      Beheer welke andere Klonkt-sites je in je cirkel toont, en of jouw site in
+      cirkels van anderen mag verschijnen.
+    </p>
+    <p><a href="/admin/circle" class="btn btn-primary">Beheer je cirkel &rarr;</a></p>
+  </section>
+  <% } %>
 </div>
 
Index: src/views/pages/circle-feed.ejs
===================================================================
--- src/views/pages/circle-feed.ejs	(revision 0091cb77d389e73218336782b310d2f000e2579a)
+++ src/views/pages/circle-feed.ejs	(revision 0091cb77d389e73218336782b310d2f000e2579a)
@@ -0,0 +1,65 @@
+<div class="container circle-page">
+  <header class="circle-head">
+    <h1>Cirkel</h1>
+    <p class="circle-sub">Wat er speelt bij de Klonkt-sites in jouw cirkel.</p>
+  </header>
+
+  <% if (!posts.length) { %>
+    <p class="circle-empty">Nog niets in je cirkel. Voeg sites toe via
+      <a href="/admin/circle">Beheer &rsaquo; Cirkel</a> en ververs ze.</p>
+  <% } else { %>
+    <div class="circle-grid">
+      <% posts.forEach(function(p){ %>
+        <article class="circle-card">
+          <% if (p.image) { %>
+            <a class="cc-cover" <% if (p.url) { %>href="<%= p.url %>" target="_blank" rel="noopener"<% } %>>
+              <img loading="lazy" src="<%= p.image %>" alt="">
+            </a>
+          <% } %>
+          <div class="cc-body">
+            <div class="cc-src">
+              <% if (p.actorAvatar) { %><img class="cc-avatar" src="<%= p.actorAvatar %>" alt=""><% } %>
+              <% if (p.actorUrl) { %>
+                <a href="<%= p.actorUrl %>" target="_blank" rel="noopener"><%= p.actorName %></a>
+              <% } else { %><span><%= p.actorName %></span><% } %>
+            </div>
+            <h2 class="cc-title">
+              <% if (p.url) { %><a href="<%= p.url %>" target="_blank" rel="noopener"><%= p.title %></a>
+              <% } else { %><%= p.title %><% } %>
+            </h2>
+            <% if (p.summary) { %><p class="cc-summary"><%= p.summary %></p><% } %>
+            <div class="cc-foot">
+              <% if (p.published) { %><span class="cc-date"><%= p.published.slice(0,10) %></span><% } %>
+              <% if (p.url) { %><a class="cc-link" href="<%= p.url %>" target="_blank" rel="noopener">Lees bij de bron &rarr;</a><% } %>
+            </div>
+          </div>
+        </article>
+      <% }); %>
+    </div>
+  <% } %>
+</div>
+
+<style>
+.circle-page { max-width: 1000px; margin: 2.5rem auto; padding: 0 1rem; }
+.circle-head h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 .25rem; }
+.circle-sub { color: var(--ink-muted); margin: 0 0 2rem; }
+.circle-empty { color: var(--ink-muted); }
+.circle-empty a { color: var(--accent); }
+.circle-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.1rem; }
+.circle-card { display: flex; flex-direction: column; border: 1px solid var(--rule); border-radius: 12px; overflow: hidden; background: var(--paper-2); }
+.cc-cover { display: block; aspect-ratio: 16/9; overflow: hidden; }
+.cc-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
+.cc-body { padding: .9rem 1rem 1rem; display: flex; flex-direction: column; gap: .5rem; }
+.cc-src { display: flex; align-items: center; gap: .45rem; font-size: .8rem; color: var(--ink-muted); }
+.cc-src a { color: var(--ink-muted); text-decoration: none; }
+.cc-src a:hover { color: var(--accent); }
+.cc-avatar { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }
+.cc-title { font-family: var(--font-display, serif); font-size: 1.15rem; margin: 0; line-height: 1.25; }
+.cc-title a { color: var(--ink); text-decoration: none; }
+.cc-title a:hover { color: var(--accent); }
+.cc-summary { margin: 0; font-size: .9rem; color: var(--ink-soft, var(--ink-muted)); line-height: 1.5;
+  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
+.cc-foot { display: flex; justify-content: space-between; align-items: center; gap: .5rem; margin-top: auto; padding-top: .35rem; font-size: .78rem; }
+.cc-date { color: var(--ink-muted); }
+.cc-link { color: var(--accent); text-decoration: none; white-space: nowrap; }
+</style>
