Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 14f7cb2a1b5add6334373b9865b7bfb7e4e2bc66)
+++ src/routes/posts.js	(revision 68a4d1be062424c6e3fd9e56ce38c67ba4a5549b)
@@ -869,6 +869,9 @@
       })
       .catch((e) => console.warn('[AP] remote boost failed:', e.message));
-    // De tussentabel meteen, zodat de knop klopt voordat de resolve terug is.
-    ActivityPubService.setMyReaction(site.slug, uri, 'boost', on);
+    // Meteen zetten, zodat de knop klopt voordat de resolve terug is. Via
+    // setReaction en niet via setMyReaction: ook dit korte moment mag geen
+    // halve schrijfactie zijn. De resolve hierboven werkt hem daarna bij met de
+    // note, zodat de post ook in je tijdlijn belandt.
+    ActivityPubService.setReaction(site.slug, uri, 'boost', on);
   }
   if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on });
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 14f7cb2a1b5add6334373b9865b7bfb7e4e2bc66)
+++ src/services/ActivityPubService.js	(revision 68a4d1be062424c6e3fd9e56ce38c67ba4a5549b)
@@ -3396,5 +3396,14 @@
   try { if (!_cirkelMembers) _cirkelMembers = db.prepare('SELECT name, url, icon FROM ap_following WHERE slug = ? AND auto_boost = 1 ORDER BY name'); return _cirkelMembers.all(slug); } catch { return []; }
 }
-// Mark a timeline post as boosted so it shows in the Cirkel (mixed by date).
+// AFGELEIDE, GEEN BRON (shaer-9e9). De waarheid over "heb ik hierop gereageerd"
+// staat in ap_my_reactions; deze vlaggen worden daaruit bijgehouden door
+// setReaction en door niets anders. Roep ze niet los aan -- dan schrijf je de
+// helft, en dat is precies hoe shaer:liked maandenlang false bleef (04aca12).
+//
+// ap_timeline.boosted verdient zijn bestaan wel: hij staat in de WHERE van de
+// Cirkel-feed (getCirkelPosts) en in boostedCount, dus hij is een index en geen
+// kopie. ap_timeline.liked wordt nergens als verzameling bevraagd en kan weg
+// zodra fase 2 lang genoeg goed staat; hij is nu nog het vangnet waarmee
+// terugdraaien een code-revert blijft in plaats van dataherstel.
 let _markBoost, _unmarkBoost, _boostedCount;
 export function markBoosted(slug, noteId) {
@@ -4841,5 +4850,5 @@
   buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
   followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
-  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
+  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
   listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
@@ -4847,5 +4856,5 @@
   gateOutgoingFollow, performApprovedFollow,
   parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
-  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, setReaction, getReaction, getReactionsFor, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
+  autoBoostCount, boostedCount, setReaction, getReaction, getReactionsFor, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
   getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
   deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
Index: test/reactions-characterization.test.js
===================================================================
--- test/reactions-characterization.test.js	(revision 14f7cb2a1b5add6334373b9865b7bfb7e4e2bc66)
+++ test/reactions-characterization.test.js	(revision 68a4d1be062424c6e3fd9e56ce38c67ba4a5549b)
@@ -254,2 +254,21 @@
   assert.equal(AP.getReactionsFor('', [uri('g3')]).size, 0);
 });
+
+// ── Fase 3: het publieke oppervlak is versmald ───────────────────────────
+
+test('de primitieven zijn niet meer bereikbaar via het service-object', async () => {
+  // Routes doen `import ActivityPubService from ...` en werken dus met het
+  // default-object. Zolang markLiked daar in staat, kan een aanroeper de helft
+  // schrijven -- en dat is niet hypothetisch: precies zo bleef shaer:liked
+  // maandenlang false. De named exports blijven bestaan voor intern gebruik en
+  // voor deze tests.
+  const svc = (await import('../src/services/ActivityPubService.js')).default;
+  for (const naam of ['markLiked', 'unmarkLiked', 'markBoosted', 'unmarkBoosted',
+                      'setMyReaction', 'getMyReactions', 'getTimelineReaction']) {
+    assert.equal(svc[naam], undefined, `${naam} hoort niet op het publieke oppervlak te staan`);
+  }
+  // Wat er WEL hoort te staan: het ene schrijfpad en het ene leespad.
+  for (const naam of ['setReaction', 'getReaction', 'getReactionsFor']) {
+    assert.equal(typeof svc[naam], 'function', `${naam} hoort er wel te zijn`);
+  }
+});
