Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 42e761627824b4a102d921032c45707abeaa8532)
+++ src/routes/activitypub.js	(revision de89079d54feaf1fd4748e90006fb3a84ebbc096)
@@ -191,4 +191,8 @@
         emojis: (() => { try { return t.reblog_emoji_json ? JSON.parse(t.reblog_emoji_json) : undefined; } catch { return undefined; } })(),
       } : undefined,
+      // Whether THIS account already liked/boosted the note, so the app's
+      // detail-view buttons show the current state (and can toggle/undo).
+      'shaer:liked': !!t.liked,
+      'shaer:boosted': !!t.boosted,
     },
   }));
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 42e761627824b4a102d921032c45707abeaa8532)
+++ src/services/ActivityPubService.js	(revision de89079d54feaf1fd4748e90006fb3a84ebbc096)
@@ -278,5 +278,10 @@
         ? (JSON.parse(post.to_actors || '[]'))
         : (post.to_actor ? [post.to_actor] : [PUBLIC]),
-      cc: post.visibility === 'direct' ? [] : [PUBLIC, `${meR}/followers`],
+      // Followers-only reply ('friends', shaer detail-view Reply): the parent
+      // author (in `to`) + our followers, but NO Public — it does not federate
+      // into open discovery. Default reply stays quiet-public (Public in cc).
+      cc: post.visibility === 'direct' ? []
+        : post.visibility === 'friends' ? [`${meR}/followers`]
+          : [PUBLIC, `${meR}/followers`],
       // FEP-633c 5.2.1: a ward's call for help. Only ever on direct notes.
       ...Guardianship.helpRequestProps(post),
@@ -2145,5 +2150,8 @@
           const parent = await resolveRemoteNote(c2sIdOf(object.inReplyTo)).catch(() => null);
           if (!parent) return { status: 502, error: 'cannot_resolve_inReplyTo' };
-          const r = await deliverReply(site, { postId: parent.localPostId || '', postSlug: null, parent, text: plain });
+          // Honour the client's visibility for the reply: 'friends' (followers-
+          // only, the Shaer detail-view Reply) drops Public; anything else stays
+          // quiet-public. 'direct' was already handled above.
+          const r = await deliverReply(site, { postId: parent.localPostId || '', postSlug: null, parent, text: plain, visibility: c2sVisibility(object) });
           if (!r || !r.id) return { status: 502, error: 'reply_failed' };
           return { status: 201, id: r.id, url: `${base}/ap/notes/${r.id}` };
@@ -2257,5 +2265,5 @@
 // Send a reply FROM this site to a remote actor (in reply to their inbound reply).
 // `parent` = an ap_interactions row (actor_uri, actor_url, actor_handle, object_uri).
-export async function deliverReply(site, { postId, postSlug, parent, text, html, language, attachments, mentions }) {
+export async function deliverReply(site, { postId, postSlug, parent, text, html, language, attachments, mentions, visibility }) {
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   // Rich replies: `html` is the reply editor's HTML (sanitized here); `text` is
@@ -2328,4 +2336,7 @@
   const id = crypto.randomUUID();
   iStmts().insO.run(id, site.slug, postId, postSlug || null, parent.object_uri || null, toActorUri, toHandle, content, replyLang, mediaJson);
+  // Followers-only reply (shaer detail-view): mark the row so buildNote drops
+  // Public from cc. Default (undefined/'public'/'quiet') stays quiet-public.
+  if (visibility === 'friends') { try { db.prepare('UPDATE ap_outbox SET visibility = ? WHERE id = ?').run('friends', id); } catch { /* ignore */ } }
   const row = iStmts().getO.get(id);
   const note = buildReplyNote(base, site, row);
Index: test/c2s-reply-visibility.test.js
===================================================================
--- test/c2s-reply-visibility.test.js	(revision de89079d54feaf1fd4748e90006fb3a84ebbc096)
+++ test/c2s-reply-visibility.test.js	(revision de89079d54feaf1fd4748e90006fb3a84ebbc096)
@@ -0,0 +1,41 @@
+// A Shaer detail-view Reply is followers-only: buildNote(isReply) with
+// visibility 'friends' addresses the parent author + followers, but NOT Public.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+const dbMod = await import('../src/config/database.js');
+dbMod.initializeDatabase();
+const { buildNote } = await import('../src/services/ActivityPubService.js');
+
+const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
+const base = 'https://test.example';
+const site = { slug: 'kid' };
+const row = (visibility) => ({
+  id: 'reply-1', in_reply_to: 'https://mastodon.social/@alice/1',
+  content: '<p>hi</p>', to_actor: 'https://mastodon.social/users/alice', visibility,
+});
+
+test('followers-only reply: author in to, followers in cc, NO Public', () => {
+  const note = buildNote(base, site, row('friends'), { isReply: true });
+  assert.deepEqual(note.to, ['https://mastodon.social/users/alice']);
+  assert.deepEqual(note.cc, [`${base}/ap/users/kid/followers`]);
+  assert.ok(!note.cc.includes(PUBLIC));
+  assert.equal(note.inReplyTo, 'https://mastodon.social/@alice/1');
+});
+
+test('default reply stays quiet-public: author in to, Public + followers in cc', () => {
+  const note = buildNote(base, site, row(null), { isReply: true });
+  assert.deepEqual(note.to, ['https://mastodon.social/users/alice']);
+  assert.ok(note.cc.includes(PUBLIC));
+  assert.ok(note.cc.includes(`${base}/ap/users/kid/followers`));
+});
+
+test('direct note addresses only its recipients (no Public/followers)', () => {
+  const note = buildNote(base, site, {
+    id: 'dm-1', content: '<p>x</p>', visibility: 'direct',
+    to_actors: JSON.stringify(['https://s/u/bob']),
+  }, { isReply: true });
+  assert.deepEqual(note.to, ['https://s/u/bob']);
+  assert.deepEqual(note.cc, []);
+});
