Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision a9da2c04639cc50ea277110a4eac16c934999834)
+++ src/routes/activitypub.js	(revision 67f71503172e1c48fc93ee0e7ea1fbc6f67e587e)
@@ -110,5 +110,15 @@
      ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20`
   ).all(site.id);
-  AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, posts), audience === 'friend' ? 'private, no-store' : undefined);
+  const ob = AP.buildOutbox(baseUrl(req), site, posts);
+  if (audience === 'friend') {
+    // The owner's app builds its feed from this leg, and every note here is
+    // by the site itself: give it the same `shaer:author` byline the timeline
+    // entries carry, so your own cards get a header too (avatar + name).
+    const me = AP.selfAuthor(baseUrl(req), site);
+    for (const it of ob.orderedItems) {
+      if (it && it.object && typeof it.object === 'object') it.object['shaer:author'] = me;
+    }
+  }
+  AP.sendAP(res, ob, audience === 'friend' ? 'private, no-store' : undefined);
 });
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision a9da2c04639cc50ea277110a4eac16c934999834)
+++ src/services/ActivityPubService.js	(revision 67f71503172e1c48fc93ee0e7ea1fbc6f67e587e)
@@ -760,4 +760,20 @@
   if (user) out.preferredUsername = user;
   if (d.icon) out.icon = { type: 'Image', url: d.icon };
+  return out;
+}
+
+// The site's OWN display info in the same shape as `shaer:author` on timeline
+// entries. The owner's app reads its own posts from the outbox, which carried
+// no author info, so every card but your own had a byline (Robins melding,
+// 30-7: geen header van self op eigen posts).
+export function selfAuthor(base, site) {
+  const out = {
+    name: site.title || site.slug,
+    handle: `@${site.slug}@${String(base).replace(/^https?:\/\//, '')}`,
+    url: `${base}/${site.slug === site.primary_slug ? '' : 'user/' + encodeURIComponent(site.slug)}`,
+  };
+  if (site.profile_photo) {
+    out.icon = /^https?:/.test(site.profile_photo) ? site.profile_photo : `${base}${site.profile_photo.startsWith('/') ? '' : '/'}${site.profile_photo}`;
+  }
   return out;
 }
@@ -2374,5 +2390,32 @@
         return { status: 400, error: 'unsupported_undo' };
       }
-      // Delete/Update of arbitrary objects need the post-edit pipeline; tracked
+      // Delete your OWN note (Robins verzoek, 30-7: long-press delete in de
+      // app). Scope stays narrow: this account's posts and outbound replies,
+      // nothing else. The web delete route is the model: Tombstone to the
+      // followers first, then the cascade, so nobody keeps a live copy of a
+      // post the child took back.
+      case 'Delete': {
+        const targetUri = c2sIdOf(object);
+        if (!targetUri) return { status: 400, error: 'missing_object' };
+        const pid = postIdFromNoteUrl(targetUri, base);
+        if (pid) {
+          const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(pid);
+          if (post) {
+            if (post.site_id !== site.id) return { status: 403, error: 'not_your_note' };
+            if (post.status === 'published') deliverDelete(site, post).catch(() => { /* best-effort */ });
+            db.transaction(() => {
+              db.prepare('DELETE FROM comments WHERE post_id = ?').run(post.id);
+              try { db.prepare('DELETE FROM posts_fts WHERE post_id = ?').run(post.id); } catch { /* FTS optional */ }
+              db.prepare('DELETE FROM posts WHERE id = ?').run(post.id);
+            })();
+            return { status: 202, url: targetUri };
+          }
+          // Same /ap/notes/ namespace: one of our outbound replies/messages.
+          // deliverOutboxDelete checks the site itself and tombstones too.
+          if (await deliverOutboxDelete(site, pid)) return { status: 202, url: targetUri };
+        }
+        return { status: 404, error: 'not_your_note' };
+      }
+      // Update of arbitrary objects needs the post-edit pipeline; tracked
       // separately (klonkt-demo-c2s-del). Reject clearly rather than half-doing it.
       default:
@@ -4114,4 +4157,4 @@
   linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
   noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget,
-  getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility, actorDisplay, buildActorRef, prefersEnriched,
+  getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility, actorDisplay, buildActorRef, prefersEnriched, selfAuthor,
 };
Index: test/c2s-compose.test.js
===================================================================
--- test/c2s-compose.test.js	(revision a9da2c04639cc50ea277110a4eac16c934999834)
+++ test/c2s-compose.test.js	(revision 67f71503172e1c48fc93ee0e7ea1fbc6f67e587e)
@@ -144,4 +144,41 @@
 });
 
+test('C2S Delete takes an own post back, and only an own post', async () => {
+  // Long-press delete in the app (Robins verzoek, 30-7): the child changes
+  // their mind, the post goes, followers get the Tombstone.
+  const r = await AP.ingestOutboxActivity(site, user, {
+    type: 'Create',
+    object: {
+      type: 'Note', content: '<p>weg hiermee</p>',
+      to: ['https://test.example/ap/users/kid/followers'],
+      cc: ['https://www.w3.org/ns/activitystreams#Public'],
+    },
+  });
+  assert.equal(r.status, 201);
+  const del = await AP.ingestOutboxActivity(site, user, { type: 'Delete', object: r.url });
+  assert.equal(del.status, 202, 'an own note deletes');
+  assert.equal(db.prepare('SELECT COUNT(*) c FROM posts WHERE id = ?').get(r.id).c, 0, 'and the row is gone');
+
+  const unknown = await AP.ingestOutboxActivity(site, user, { type: 'Delete', object: 'https://test.example/ap/notes/bestaat-niet' });
+  assert.equal(unknown.status, 404, 'an unknown note is a clear no');
+
+  // Another account's post on this server: refused, row untouched.
+  db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u2', 'ander', 'u2@t', 'x', 'user');
+  db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,0)').run('s2', 'ander', 'ander', 'u2');
+  db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at, created_at, updated_at)
+              VALUES ('p-ander', 's2', 'u2', 'n-ander', '', '<p>van een ander</p>', 'published', datetime('now'), datetime('now'), datetime('now'))`).run();
+  const foreign = await AP.ingestOutboxActivity(site, user, { type: 'Delete', object: 'https://test.example/ap/notes/p-ander' });
+  assert.equal(foreign.status, 403, "someone else's post is not yours to take back");
+  assert.equal(db.prepare('SELECT COUNT(*) c FROM posts WHERE id = ?').get('p-ander').c, 1, 'and it stays');
+});
+
+test('selfAuthor: the byline for your own outbox notes', () => {
+  // The owner's app reads its own posts from the outbox, which carried no
+  // author info: every card but your own had a header (Robins melding, 30-7).
+  const me = AP.selfAuthor('https://test.example', site);
+  assert.equal(me.name, 'kid');
+  assert.equal(me.handle, '@kid@test.example');
+});
+
 test('a media-only post is a post, not an empty-note error', async () => {
   const r = await AP.ingestOutboxActivity(site, user, {
