Index: src/services/Scheduler.js
===================================================================
--- src/services/Scheduler.js	(revision 834bcc3c41c52321679550863b74ca035538659f)
+++ src/services/Scheduler.js	(revision 5a6a457ca2bf625de84ec03d20f7757480337fde)
@@ -10,9 +10,11 @@
 import db from '../config/database.js';
 import HtmlSanitizerService from './HtmlSanitizerService.js';
+import ActivityPubService from './ActivityPubService.js';
 
 export function flipScheduledPosts() {
   try {
     const due = db.prepare(`
-      SELECT p.id, p.title, p.content, u.username
+      SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only,
+             p.published_at, p.publish_at, p.created_at, u.username
       FROM posts p JOIN users u ON u.id = p.author_id
       WHERE p.status = 'scheduled' AND p.publish_at IS NOT NULL AND datetime(p.publish_at) <= datetime('now')
@@ -23,7 +25,21 @@
     );
     const fts = db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?, ?, ?, ?)');
+    const siteStmt = db.prepare('SELECT * FROM sites WHERE id = ?');
     for (const p of due) {
       upd.run(p.id);
       try { fts.run(HtmlSanitizerService.toPlainText(p.content || ''), p.title || '', p.username || '', p.id); } catch { /* FTS failure is non-fatal */ }
+      // ActivityPub: federate the now-published post to followers.
+      if (!p.fan_only) {
+        try {
+          const site = siteStmt.get(p.site_id);
+          if (site) {
+            ActivityPubService.deliverCreate(site, {
+              id: p.id, slug: p.slug, title: p.title || p.slug,
+              content: p.content, cover_image_url: p.cover_image_url || null,
+              published_at: p.published_at || p.publish_at, created_at: p.created_at,
+            }).catch(() => { /* best-effort */ });
+          }
+        } catch { /* non-fatal */ }
+      }
     }
     return due.length;
