Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision f926d13c4545a33246387cc7a96c0fd8fa918942)
+++ src/services/ActivityPubService.js	(revision 47db4b55ef613863bde81368d52e3cce66e8969d)
@@ -5795,8 +5795,24 @@
     }
   }
+  // Een Move is een Move: de guardian is dezelfde guardian, het kind is hetzelfde
+  // kind, alleen het adres is nieuw. Toezicht IS een gewone Follow (FEP-633c §5),
+  // en die is hierboven al meeverhuisd -- maar de relatie zelf hing nog aan de
+  // oude URI. Dat leverde de vervelendste toestand op: de guardian ziet de posts
+  // nog binnenkomen, terwijl alles dat op other_uri matcht omvalt (de gate op een
+  // nieuwe volger, het escalatiepad, listGuardians bij het kind). Half een
+  // vangnet ziet eruit als een heel vangnet.
+  //
+  // Zelfde bescherming als de re-follow hierboven: alleen na een geverifieerde
+  // Move, en niet als de bestemming geblokkeerd is (daar zijn we al uitgestapt).
+  // De twee harde randen van shaer-tge staan hier LOS van: weigeren te verhuizen
+  // naar een instance die shaer:guardians niet kan dragen is een controle aan de
+  // UITGAANDE kant, en het terugkeren-zonder-set is een alsoKnownAs-kwestie.
   try {
     const g = db.prepare('SELECT slug, role FROM ap_guardianships WHERE other_uri = ? AND status = ?').all(oldUri, 'accepted');
-    if (g.length) console.warn('[AP] Move touches a guardianship party — left untouched (shaer-tge):', oldUri, '→', g.map((r) => `${r.role}:${r.slug}`).join(', '));
-  } catch { /* table absent on fresh init */ }
+    if (g.length) {
+      const r = db.prepare('UPDATE ap_guardianships SET other_uri = ? WHERE other_uri = ? AND status = ?').run(newUri, oldUri, 'accepted');
+      console.log('[AP] guardianship moved:', oldUri, '→', newUri, `(${r.changes}x)`, g.map((x) => `${x.role}:${x.slug}`).join(', '));
+    }
+  } catch (e) { console.warn('[AP] guardianship move failed:', e && e.message); }
   return 202;
 }
Index: test/move-guardianship.test.js
===================================================================
--- test/move-guardianship.test.js	(revision 47db4b55ef613863bde81368d52e3cce66e8969d)
+++ test/move-guardianship.test.js	(revision 47db4b55ef613863bde81368d52e3cce66e8969d)
@@ -0,0 +1,100 @@
+// Een Move is een Move: de guardian blijft dezelfde guardian.
+//
+// Toezicht is een gewone Follow (FEP-633c §5), en die verhuisde al mee. Maar de
+// guardianship-RELATIE bleef aan de oude URI hangen. Dat gaf de vervelendste
+// toestand van allemaal: de guardian ziet de posts van het kind gewoon
+// binnenkomen, terwijl alles dat op other_uri matcht omvalt. Half een vangnet
+// ziet eruit als een heel vangnet, en dan merk je het pas als het nodig is.
+import { test, beforeEach } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://ons.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+{
+  const stil = console.log;
+  console.log = () => {};
+  try { dbMod.initializeDatabase(); } finally { console.log = stil; }
+}
+const AP = await import('../src/services/ActivityPubService.js');
+
+const OUD = 'https://oud.example/ap/users/kind';
+const NIEUW = 'https://nieuw.example/ap/users/kind';
+
+// De guardian zijn WIJ: een lokale site die het kind volgt en de relatie draagt.
+function zetKlaar({ status = 'accepted' } = {}) {
+  db.prepare('DELETE FROM ap_following').run();
+  db.prepare('DELETE FROM ap_guardianships').run();
+  db.prepare('INSERT OR IGNORE INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+    .run('u1', 'u1', 'u1@test', 'x', 'god');
+  db.prepare('INSERT OR IGNORE INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
+    .run('s1', 'voogd', 'De voogd', 'u1');
+  db.prepare(`INSERT INTO ap_following (slug, actor_uri, handle, status, auto_boost)
+              VALUES (?,?,?,?,?)`).run('voogd', OUD, '@kind@oud.example', 'accepted', 1);
+  db.prepare(`INSERT INTO ap_guardianships (slug, other_uri, role, status)
+              VALUES (?,?,?,?)`).run('voogd', OUD, 'guardian', status);
+}
+
+const relatie = () => db.prepare('SELECT other_uri, role, status FROM ap_guardianships').all();
+
+// Geen netwerk: de Move-afhandeling accepteert injecteerbare functies.
+const stubs = {
+  verifiedActor: OUD,
+  fetchActorFn: async (uri) => ({ id: uri, inbox: `${uri}/inbox`, alsoKnownAs: [OUD] }),
+  followFn: async () => true,
+  unfollowFn: async () => true,
+};
+const move = { type: 'Move', actor: OUD, object: OUD, target: NIEUW };
+
+beforeEach(() => zetKlaar());
+
+test('na een Move wijst de guardianship naar het NIEUWE adres', async () => {
+  const stil = console.log; console.log = () => {};
+  try { await AP.handleMoveInbox(move, stubs); } finally { console.log = stil; }
+
+  const r = relatie();
+  assert.equal(r.length, 1, 'de relatie mag niet verdubbelen of verdwijnen');
+  assert.equal(r[0].other_uri, NIEUW, 'anders ziet de guardian de posts wel, maar valt de gate om');
+  assert.equal(r[0].role, 'guardian', 'de rol verandert niet: hij is dezelfde guardian');
+  assert.equal(r[0].status, 'accepted');
+});
+
+test('de Follow verhuist mee, met de uitgelicht-stand', async () => {
+  const gevolgd = [];
+  const stil = console.log; console.log = () => {};
+  try {
+    await AP.handleMoveInbox(move, {
+      ...stubs,
+      followFn: async (site, uri, boost) => { gevolgd.push([uri, boost]); return true; },
+    });
+  } finally { console.log = stil; }
+  assert.deepEqual(gevolgd, [[NIEUW, true]]);
+});
+
+test('een niet-geverifieerde Move raakt de guardianship niet aan', async () => {
+  const stil = console.warn; console.warn = () => {};
+  try {
+    // verifiedActor wijst niet naar de oude actor: dit is niet zijn Move.
+    await AP.handleMoveInbox(move, { ...stubs, verifiedActor: 'https://iemand.anders/ap/users/x' });
+  } finally { console.warn = stil; }
+  assert.equal(relatie()[0].other_uri, OUD,
+    'anders kan een vreemde het kind bij zijn guardians weghalen door een Move te sturen');
+});
+
+test('een relatie die nog niet geaccepteerd is blijft staan', async () => {
+  zetKlaar({ status: 'pending' });
+  const stil = console.log; console.log = () => {};
+  try { await AP.handleMoveInbox(move, stubs); } finally { console.log = stil; }
+  assert.equal(relatie()[0].other_uri, OUD,
+    'een verzoek dat nog loopt gaat over de OUDE actor; dat verhuizen zou een niet-bestaande relatie meenemen');
+});
+
+test('een Move naar een geblokkeerde bestemming verandert niets', async () => {
+  db.prepare('INSERT INTO ap_blocks (slug, target, kind) VALUES (?,?,?)').run('voogd', NIEUW, 'actor');
+  const stil = console.log; console.log = () => {};
+  try { await AP.handleMoveInbox(move, stubs); } finally { console.log = stil; }
+  db.prepare('DELETE FROM ap_blocks').run();
+  assert.equal(relatie()[0].other_uri, OUD, 'geblokkeerd is geblokkeerd, ook voor een guardianship');
+});
