Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision bb9e54cec02ebc6188104575f97e2d80a9b52679)
+++ src/routes/activitypub.js	(revision e311202e61b8a8e0ce2a0f0f23d873495f89d76f)
@@ -1258,4 +1258,13 @@
   // cannot_resolve_inReplyTo. Strangers still get the exact same 404, so a
   // note's existence stays as private as before.
+  // EEN GEBLOKKEERDE KRIJGT DE DEUR DICHT (Robin, 21-8), net als bij de outbox:
+  // wie ondertekend aanklopt, klopt met zijn naam erop, en een blokkade is een
+  // gesloten deur. Dezelfde 404 als een vreemde, zodat het bestaan van een note
+  // niets extra's verraadt. Onbetekende verzoeken kunnen we niet thuisbrengen
+  // en houden de publieke weergave -- daarvoor is de Block-bezorging.
+  if (req.headers['signature']) {
+    const wie = await AP.verifyRequest(req).catch(() => null);
+    if (wie && wie.id && AP.isBlockedAny(wie.id)) return res.status(404).end();
+  }
   const post = db.prepare(
     "SELECT * FROM posts WHERE id = ? AND status = 'published'"
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision bb9e54cec02ebc6188104575f97e2d80a9b52679)
+++ src/routes/posts.js	(revision e311202e61b8a8e0ce2a0f0f23d873495f89d76f)
@@ -1534,5 +1534,5 @@
 router.post('/blocking/remove', requireSiteManager, (req, res) => {
   const site = res.locals.site;
-  if (site) { try { ActivityPubService.unblock(site, (req.body.target || '').toString()); } catch (e) { /* ignore */ } }
+  if (site) { try { ActivityPubService.unblock(site, (req.body.target || '').toString()).catch(() => {}); } catch (e) { /* ignore */ } }
   res.redirect('/blocking?success=' + encodeURIComponent('Deblokkeerd'));
 });
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision bb9e54cec02ebc6188104575f97e2d80a9b52679)
+++ src/services/ActivityPubService.js	(revision e311202e61b8a8e0ce2a0f0f23d873495f89d76f)
@@ -3600,5 +3600,5 @@
         if (innerType === 'Block') {
           if (!innerTarget) return { status: 400, error: 'missing_object' };
-          unblock(site, innerTarget);   // release from Orbit
+          unblock(site, innerTarget).catch(() => {});   // release from Orbit
           return { status: 202, url: innerTarget };
         }
@@ -6666,7 +6666,24 @@
 // Block an actor (@handle or actor URL) or a whole domain; purges their content.
 // The handle resolver is ours; the storage/purge lives in BlocklistService.
-export async function blockTarget(site, input) { return Blocklist.blockTarget(site, input, webfingerResolve); }
-
-export function unblock(site, target) { return Blocklist.unblock(site, target); }
+//
+// De BEZORGING hoort ook hier: BlocklistService kent de database, niet het
+// afleveren. Een Block gaat naar de inbox van wie je blokkeert, een
+// Undo(Block) bij het opheffen -- zonder retry-wachtrij, want een blokkade
+// wacht niet op een server die even plat ligt (en bij opheffen komt de ander
+// vanzelf weer langs).
+async function bezorgBlokkade(site, target, undo) {
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  const me = actorId(base, site.slug);
+  const blok = { id: `${me}#block-${Date.now()}-${rid()}`, type: 'Block', actor: me, object: target, to: [target] };
+  const activiteit = undo
+    ? { '@context': AP_CONTEXT, id: `${me}#unblock-${Date.now()}-${rid()}`, type: 'Undo', actor: me, object: blok, to: [target] }
+    : { '@context': AP_CONTEXT, ...blok };
+  const r = await deliverToActor(site, target, activiteit);
+  console.log('[AP]', undo ? 'Undo(Block)' : 'Block', site.slug, '→', target, r && r.delivered ? 'bezorgd' : 'niet bezorgd');
+}
+
+export async function blockTarget(site, input) { return Blocklist.blockTarget(site, input, webfingerResolve, bezorgBlokkade); }
+
+export function unblock(site, target) { return Blocklist.unblock(site, target, bezorgBlokkade); }
 
 // ── Guardianship module wiring (src/services/guardianship/) ────────
Index: src/services/BlocklistService.js
===================================================================
--- src/services/BlocklistService.js	(revision bb9e54cec02ebc6188104575f97e2d80a9b52679)
+++ src/services/BlocklistService.js	(revision e311202e61b8a8e0ce2a0f0f23d873495f89d76f)
@@ -56,5 +56,24 @@
 // `resolveHandle` (async handle → actor URL) is injected by the caller so this
 // service needs nothing from ActivityPubService (no circular import).
-export async function blockTarget(site, input, resolveHandle) {
+/**
+ * Een blokkade is pas een blokkade als de ander het merkt (Robin, 21-8).
+ *
+ * Tot vandaag bleef hij binnenshuis: rij in ap_blocks, inhoud opruimen, volger
+ * eruit -- en verder niets. De andere kant volgde je dan nog steeds in zijn
+ * eigen boeken en bleef je publieke outbox lezen. Precies wat de hub deed:
+ * kanaal en berichten stonden er gewoon nog. Vandaar dat we het nu ook
+ * VERSTUREN, zoals Mastodon dat doet: een Block naar de inbox van wie je
+ * blokkeert, en bij opheffen een Undo(Block) zodat de weg terug openligt.
+ *
+ * Alleen voor een actor-blokkade: een heel domein heeft geen inbox om aan te
+ * schrijven. En bezorgen mag nooit de blokkade zelf tegenhouden -- die staat
+ * al vast in de database voordat we ook maar iets proberen te versturen.
+ */
+async function meldBlokkade(site, target, kind, bezorg, undo = false) {
+  if (kind !== 'actor' || typeof bezorg !== 'function') return;
+  try { await bezorg(site, target, undo); } catch { /* de blokkade staat, de melding is een gunst */ }
+}
+
+export async function blockTarget(site, input, resolveHandle, bezorg) {
   const raw = String(input || '').trim();
   if (!site || !site.slug || !raw) return { error: 'empty' };
@@ -69,8 +88,16 @@
   purgeBlocked(kind, target);
   console.log('[AP] block', site.slug, kind, target);
+  await meldBlokkade(site, target, kind, bezorg);
   return { ok: true, label };
 }
 
-export function unblock(site, target) { blStmts().del.run(site.slug, target); return { ok: true }; }
+export async function unblock(site, target, bezorg) {
+  const rij = blStmts().list.all(site.slug).find((b) => b.target === target);
+  blStmts().del.run(site.slug, target);
+  // Undo(Block) zodat de ander weet dat de deur weer open is; zonder dit blijft
+  // hij bij zichzelf geblokkeerd staan en komt hij nooit terug.
+  await meldBlokkade(site, target, (rij && rij.kind) || 'actor', bezorg, true);
+  return { ok: true };
+}
 
 export default { listBlocks, isBlockedAny, blockTarget, unblock };
