Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/services/ActivityPubService.js	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -364,4 +364,6 @@
   // forged replies/likes/follows/timeline posts). GET/discovery stays open.
   const claimedActor = typeof act.actor === 'string' ? act.actor : (act.actor && act.actor.id);
+  // Blocked actor/domain → silently drop (202, don't reveal the block).
+  if (claimedActor && isBlockedAny(claimedActor)) { console.log('[AP] inbox dropped (blocked)', claimedActor); return 202; }
   const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject'];
   if (GATED.includes(type)) {
@@ -774,4 +776,58 @@
 }
 
+// ── Blocking / defederation ───────────────────────────────────────
+let _insBl, _delBl, _listBl;
+function blStmts() {
+  if (!_insBl) {
+    _insBl = db.prepare('INSERT OR IGNORE INTO ap_blocks (slug, target, kind, label, created_at) VALUES (?,?,?,?,CURRENT_TIMESTAMP)');
+    _delBl = db.prepare('DELETE FROM ap_blocks WHERE slug = ? AND target = ?');
+    _listBl = db.prepare('SELECT * FROM ap_blocks WHERE slug = ? ORDER BY created_at DESC');
+  }
+  return { ins: _insBl, del: _delBl, list: _listBl };
+}
+export function listBlocks(slug) { return blStmts().list.all(slug); }
+
+// True if an actor (or its whole domain) is blocked anywhere on this instance.
+export function isBlockedAny(actorUri) {
+  if (!actorUri) return false;
+  let domain = ''; try { domain = new URL(actorUri).host; } catch { /* ignore */ }
+  try { return !!db.prepare("SELECT 1 FROM ap_blocks WHERE (kind='actor' AND target=?) OR (kind='domain' AND target=?) LIMIT 1").get(actorUri, domain); }
+  catch { return false; }
+}
+
+function purgeBlocked(kind, target) {
+  try {
+    if (kind === 'domain') {
+      const like = `%//${target}/%`;
+      db.prepare('DELETE FROM ap_interactions WHERE actor_uri LIKE ?').run(like);
+      db.prepare('DELETE FROM ap_timeline WHERE author_uri LIKE ?').run(like);
+      db.prepare('DELETE FROM ap_followers WHERE actor_uri LIKE ?').run(like);
+    } else {
+      db.prepare('DELETE FROM ap_interactions WHERE actor_uri = ?').run(target);
+      db.prepare('DELETE FROM ap_timeline WHERE author_uri = ?').run(target);
+      db.prepare('DELETE FROM ap_followers WHERE actor_uri = ?').run(target);
+    }
+  } catch { /* best-effort */ }
+}
+
+// Block an actor (@handle or actor URL) or a whole domain; purges their content.
+export async function blockTarget(site, input) {
+  const raw = String(input || '').trim();
+  if (!site || !site.slug || !raw) return { error: 'empty' };
+  let kind, target, label;
+  if (/^https?:\/\//i.test(raw)) { kind = 'actor'; target = raw; label = raw; }
+  else if (raw.includes('@')) {
+    const actorUrl = await webfingerResolve(raw);
+    if (!actorUrl) return { error: 'not_found' };
+    kind = 'actor'; target = actorUrl; label = raw.startsWith('@') ? raw : ('@' + raw);
+  } else { kind = 'domain'; target = raw.toLowerCase(); label = raw.toLowerCase(); }
+  blStmts().ins.run(site.slug, target, kind, label);
+  purgeBlocked(kind, target);
+  console.log('[AP] block', site.slug, kind, target);
+  return { ok: true, label };
+}
+
+export function unblock(site, target) { blStmts().del.run(site.slug, target); return { ok: true }; }
+
 export default {
   getOrCreateKeys, apWants, sendAP, actorId, noteId,
@@ -781,4 +837,4 @@
   listOutbox, deliverOutboxDelete,
   webfingerResolve, followActor, unfollowActor, listFollowing, getTimeline, sendInteraction,
-  getNotifications,
+  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
 };
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
+++ src/services/i18n.js	(revision f5c3870ac3f184f2c36420726af8895b46458f19)
@@ -26,5 +26,5 @@
     'nav.language': 'Taal',
     'nav.notifications': 'Meldingen',
-    'notif.title': 'Meldingen', 'notif.empty': 'Nog geen meldingen.', 'notif.someone': 'Iemand', 'notif.followed': 'volgt je nu', 'notif.liked': 'likete je post', 'notif.boosted': 'boostte je post', 'notif.replied': 'reageerde op',
+    'notif.title': 'Meldingen', 'notif.empty': 'Nog geen meldingen.', 'notif.someone': 'Iemand', 'notif.followed': 'volgt je nu', 'notif.liked': 'likete je post', 'notif.boosted': 'boostte je post', 'notif.replied': 'reageerde op', 'blk.title': 'Blokkeren', 'blk.lead': 'Blokkeer een account of een heel domein — hun reacties, likes en posts verdwijnen en nieuwe worden geweigerd.', 'blk.block_btn': 'Blokkeren', 'blk.empty': 'Niks geblokkeerd.', 'blk.unblock': 'Deblokkeren', 'tl.block': 'Blokkeer',
     'notif.reply': '{actor} reageerde op je reactie', 'notif.comment': '{actor} reageerde op je post', 'notif.like': '{actor} vindt je post leuk',
     'switch.agenda': 'Agenda',
@@ -1029,5 +1029,5 @@
     'nav.language': 'Language',
     'nav.notifications': 'Notifications',
-    'notif.title': 'Notifications', 'notif.empty': 'No notifications yet.', 'notif.someone': 'Someone', 'notif.followed': 'followed you', 'notif.liked': 'liked your post', 'notif.boosted': 'boosted your post', 'notif.replied': 'replied to',
+    'notif.title': 'Notifications', 'notif.empty': 'No notifications yet.', 'notif.someone': 'Someone', 'notif.followed': 'followed you', 'notif.liked': 'liked your post', 'notif.boosted': 'boosted your post', 'notif.replied': 'replied to', 'blk.title': 'Blocking', 'blk.lead': 'Block an account or a whole domain — their replies, likes and posts disappear and new ones are refused.', 'blk.block_btn': 'Block', 'blk.empty': 'Nothing blocked.', 'blk.unblock': 'Unblock', 'tl.block': 'Block',
     'notif.reply': '{actor} replied to your comment', 'notif.comment': '{actor} commented on your post', 'notif.like': '{actor} liked your post',
     'switch.agenda': 'Agenda',
@@ -2023,5 +2023,5 @@
     'nav.language': 'Sprache',
     'nav.notifications': 'Benachrichtigungen',
-    'notif.title': 'Benachrichtigungen', 'notif.empty': 'Noch keine Benachrichtigungen.', 'notif.someone': 'Jemand', 'notif.followed': 'folgt dir jetzt', 'notif.liked': 'gefällt dein Beitrag', 'notif.boosted': 'teilte deinen Beitrag', 'notif.replied': 'antwortete auf',
+    'notif.title': 'Benachrichtigungen', 'notif.empty': 'Noch keine Benachrichtigungen.', 'notif.someone': 'Jemand', 'notif.followed': 'folgt dir jetzt', 'notif.liked': 'gefällt dein Beitrag', 'notif.boosted': 'teilte deinen Beitrag', 'notif.replied': 'antwortete auf', 'blk.title': 'Blockieren', 'blk.lead': 'Blockiere ein Konto oder eine ganze Domain — ihre Antworten, Likes und Beiträge verschwinden und neue werden abgelehnt.', 'blk.block_btn': 'Blockieren', 'blk.empty': 'Nichts blockiert.', 'blk.unblock': 'Entsperren', 'tl.block': 'Blockieren',
     'notif.reply': '{actor} hat auf deinen Kommentar geantwortet', 'notif.comment': '{actor} hat deinen Beitrag kommentiert', 'notif.like': '{actor} gefällt dein Beitrag',
     'switch.agenda': 'Termine',
