Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision e2ea5c49d99947fd02bdda292a25027f868b7225)
+++ src/routes/activitypub.js	(revision fb30b5d0f27e2c88d8c14739a2d20267e7a655ed)
@@ -112,4 +112,7 @@
       sensitive: !!t.nsfw,
       summary: t.cw || undefined,
+      // Friends' media travels along (media_json → AS2 attachment), so the
+      // client renders their images/audio like own outbox posts.
+      attachment: AP.timelineAttachments(t.media_json),
     },
   }));
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision e2ea5c49d99947fd02bdda292a25027f868b7225)
+++ src/services/ActivityPubService.js	(revision fb30b5d0f27e2c88d8c14739a2d20267e7a655ed)
@@ -2434,4 +2434,19 @@
 export function getTimeline(slug, limit, offset) { return tlStmts().list.all(slug, limit || 50, offset || 0); }
 
+// Inbox C2S read: a timeline row's media_json ([{url, type}], written on the
+// inbound Create) → AS2 `attachment` array, so a client (Shaer) can render a
+// friend's images/audio/video natively, exactly like own outbox posts. The
+// stored `type` is the mediaType and may be ''. Malformed JSON yields
+// undefined and never blocks the item.
+export function timelineAttachments(mediaJson) {
+  try {
+    const list = mediaJson ? JSON.parse(mediaJson) : [];
+    const rows = (Array.isArray(list) ? list : [])
+      .filter((m) => m && m.url)
+      .map((m) => ({ type: 'Document', mediaType: m.type || undefined, url: m.url }));
+    return rows.length ? rows : undefined;
+  } catch { return undefined; }
+}
+
 // ── Cirkel = posts from the accounts you auto-boost ("feature an artist") ──
 let _abCount, _cirkelPosts, _cirkelMembers;
@@ -3090,5 +3105,5 @@
   getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
   listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
-  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, sendInteraction, voteOnPoll, voteOnRemotePoll,
+  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, sendInteraction, voteOnPoll, voteOnRemotePoll,
   parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
   autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
