Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 6be57b4716118bcb1264cfea3f073b10963be2bf)
+++ src/config/database.js	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
@@ -82,4 +82,6 @@
   // Per-post noindex + type
   ensureColumn('posts', 'noindex', 'INTEGER DEFAULT 0');
+  ensureColumn('posts', 'publish_at', 'DATETIME');         // release-planning (premium #3): geplande go-live
+  ensureColumn('posts', 'fan_only', 'INTEGER DEFAULT 0');  // fan-only preview (premium #3)
   ensureColumn('posts', 'type',    "TEXT DEFAULT 'post'");  // post | foto | video | audio
 
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 6be57b4716118bcb1264cfea3f073b10963be2bf)
+++ src/routes/posts.js	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
@@ -154,4 +154,5 @@
 
   const { title, slug, content, excerpt, status, pinned, cover_image_url, tags, noindex, type } = req.body;
+  const fanOnly = req.body.fan_only ? 1 : 0;
 
   // Content arrives as user-authored HTML from the WYSIWYG editor — sanitize
@@ -177,13 +178,22 @@
   const postId = uuid();
   const now = new Date().toISOString();
-  const finalStatus = status || 'draft';
-  const publishedAt = finalStatus === 'published' ? now : null;
+  let finalStatus = status || 'draft';
+  let publishedAt = finalStatus === 'published' ? now : null;
+  // Release-planning: gepubliceerd + een toekomstige publish_at -> 'scheduled'
+  // (de Scheduler zet 'm live op het moment zelf). Verleden/leeg -> meteen live.
+  let publishAt = null;
+  const pa = Date.parse(req.body.publish_at || '');
+  if (finalStatus === 'published' && Number.isFinite(pa) && pa > Date.now()) {
+    finalStatus = 'scheduled';
+    publishAt = new Date(pa).toISOString();
+    publishedAt = null;
+  }
 
   db.prepare(`
     INSERT INTO posts (
       id, site_id, slug, author_id, title, content, excerpt,
-      status, cover_image_url, pinned, tags, type, noindex,
+      status, cover_image_url, pinned, tags, type, noindex, fan_only, publish_at,
       created_at, updated_at, published_at
-    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
   `).run(
     postId, site.id, finalSlug, req.session.user.id,
@@ -191,5 +201,5 @@
     finalStatus, cover_image_url || null, parsePinnedRank(pinned),
     JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
-    finalType, noindex ? 1 : 0,
+    finalType, noindex ? 1 : 0, fanOnly, publishAt,
     now, now, publishedAt
   );
@@ -255,4 +265,5 @@
 
   const { title, content, excerpt, status, pinned, cover_image_url, tags, noindex, type } = req.body;
+  const fanOnly = req.body.fan_only ? 1 : 0;
   const newSlug = req.body.slug;
   const action = req.body.action || 'save';
@@ -281,9 +292,18 @@
   }
 
+  // Release-planning: gepubliceerd + toekomstige publish_at -> 'scheduled'.
+  let publishAt = null;
+  const pa = Date.parse(req.body.publish_at || '');
+  if (finalStatus === 'published' && Number.isFinite(pa) && pa > Date.now()) {
+    finalStatus = 'scheduled';
+    publishAt = new Date(pa).toISOString();
+    publishedAt = null;
+  }
+
   db.prepare(`
     UPDATE posts SET
       title = ?, content = ?, excerpt = ?, status = ?,
       cover_image_url = ?, pinned = ?, tags = ?,
-      type = ?, noindex = ?,
+      type = ?, noindex = ?, fan_only = ?, publish_at = ?,
       slug = ?, published_at = ?, updated_at = ?
     WHERE id = ?
@@ -292,5 +312,5 @@
     cover_image_url || null, parsePinnedRank(pinned),
     JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
-    finalType, noindex ? 1 : 0,
+    finalType, noindex ? 1 : 0, fanOnly, publishAt,
     finalSlug, publishedAt, now, post.id
   );
@@ -393,4 +413,16 @@
     const canEdit = req.session?.user && PermissionsService.canEditPost(req.session.user, post, site);
     if (!canEdit) return res.status(403).send('Not published');
+  }
+
+  // Fan-only preview (premium #3): volledige inhoud alleen voor ingelogde fans.
+  // Anonieme bezoekers krijgen een nette login-gate i.p.v. de inhoud (de titel/
+  // teaser mag elders wel als lokkertje verschijnen).
+  if (post.fan_only && !(req.session && req.session.user)) {
+    return renderPage(req, res, 'pages/fan-gate', {
+      pageTitle: post.title || 'Alleen voor fans',
+      bodyClass: 'on-special',
+      fgTitle: post.title || '',
+      fgNext: (res.locals.siteUrlBase || '') + '/' + post.slug,
+    });
   }
 
Index: src/server.js
===================================================================
--- src/server.js	(revision 6be57b4716118bcb1264cfea3f073b10963be2bf)
+++ src/server.js	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
@@ -16,4 +16,5 @@
 import http from 'http';
 import db, { initializeDatabase } from './config/database.js';
+import { startScheduler } from './services/Scheduler.js';
 import { SqliteSessionStore } from './services/SqliteSessionStore.js';
 import { ensurePrimarySite } from './services/ensurePrimarySite.js';
@@ -140,4 +141,5 @@
 // → crash-loop op de allereerste boot).
 initializeDatabase();
+startScheduler(); // release-planning: zet geplande posts live zodra publish_at bereikt is
 
 // Vangnet: garandeer dat er altijd een primaire site is (solo/hub/circle).
Index: src/services/Scheduler.js
===================================================================
--- src/services/Scheduler.js	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
+++ src/services/Scheduler.js	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
@@ -0,0 +1,39 @@
+/**
+ * Scheduler — release-planning (premium #3).
+ *
+ * Geplande posts hebben status 'scheduled' + publish_at (toekomst). Een lichte
+ * timer zet ze op 'published' zodra publish_at bereikt is. Zo hoeven de publieke
+ * queries (status='published') NIET aangepast te worden — een geplande post is
+ * gewoon nog niet 'published' en dus nergens publiek zichtbaar tot het moment.
+ */
+
+import db from '../config/database.js';
+import HtmlSanitizerService from './HtmlSanitizerService.js';
+
+export function flipScheduledPosts() {
+  try {
+    const due = db.prepare(`
+      SELECT p.id, p.title, p.content, 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 p.publish_at <= CURRENT_TIMESTAMP
+    `).all();
+    if (!due.length) return 0;
+    const upd = db.prepare(
+      "UPDATE posts SET status = 'published', published_at = COALESCE(published_at, publish_at, CURRENT_TIMESTAMP) WHERE id = ?"
+    );
+    const fts = db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?, ?, ?, ?)');
+    for (const p of due) {
+      upd.run(p.id);
+      try { fts.run(HtmlSanitizerService.toPlainText(p.content || ''), p.title || '', p.username || '', p.id); } catch { /* FTS niet-fataal */ }
+    }
+    return due.length;
+  } catch { return 0; }
+}
+
+let _timer = null;
+export function startScheduler() {
+  flipScheduledPosts();                 // direct bij boot
+  if (_timer) return;
+  _timer = setInterval(flipScheduledPosts, 60 * 1000); // elke minuut
+  if (_timer.unref) _timer.unref();
+}
Index: src/views/pages/fan-gate.ejs
===================================================================
--- src/views/pages/fan-gate.ejs	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
+++ src/views/pages/fan-gate.ejs	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
@@ -0,0 +1,19 @@
+<section class="fg">
+  <div class="fg-card">
+    <div class="fg-lock">🔒</div>
+    <h1 class="fg-h1">Alleen voor fans</h1>
+    <% if (typeof fgTitle !== 'undefined' && fgTitle) { %><p class="fg-which">"<%= fgTitle %>"</p><% } %>
+    <p class="fg-sub">Dit is exclusief voor ingelogde fans. Log in om het te bekijken.</p>
+    <p><a class="fg-btn" href="/auth/login?next=<%= encodeURIComponent(fgNext || '/') %>">Inloggen / aanmelden</a></p>
+  </div>
+</section>
+
+<style>
+  .fg { max-width: 480px; margin: 0 auto; padding: 56px 18px; text-align: center; }
+  .fg-card { border: 1px solid rgba(128,128,128,.2); border-radius: 18px; padding: 36px 28px; }
+  .fg-lock { font-size: 40px; margin-bottom: 8px; }
+  .fg-h1 { font-size: 26px; margin: 0 0 6px; }
+  .fg-which { font-style: italic; opacity: .8; margin: 0 0 12px; }
+  .fg-sub { opacity: .85; line-height: 1.6; margin: 0 0 20px; }
+  .fg-btn { display: inline-block; padding: 12px 22px; border-radius: 10px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; }
+</style>
Index: src/views/pages/post-edit.ejs
===================================================================
--- src/views/pages/post-edit.ejs	(revision 6be57b4716118bcb1264cfea3f073b10963be2bf)
+++ src/views/pages/post-edit.ejs	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
@@ -193,4 +193,15 @@
           <span>🚫 noindex (verberg voor zoekmachines)</span>
         </label>
+        <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
+          <label class="pe-checkbox">
+            <input type="checkbox" name="fan_only" value="1" <%= post.fan_only ? 'checked' : '' %>>
+            <span>🔒 Alleen voor ingelogde fans</span>
+          </label>
+          <div style="margin-top:8px">
+            <label style="display:block;font-size:12.5px;opacity:.8;margin-bottom:4px">📅 Publiceren op (laat leeg = direct)</label>
+            <input type="datetime-local" name="publish_at" value="<%= post.publish_at ? String(post.publish_at).replace(' ','T').slice(0,16) : '' %>" style="padding:8px 10px;border-radius:8px;border:1px solid var(--rule,rgba(128,128,128,.4));background:transparent;color:inherit">
+            <% if (post.status === 'scheduled' && post.publish_at) { %><div style="font-size:12px;opacity:.7;margin-top:4px">⏳ Ingepland voor <%= post.publish_at %></div><% } %>
+          </div>
+        <% } %>
       </div>
     </section>
