Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
+++ src/config/database.js	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
@@ -228,4 +228,21 @@
   `);
 
+  // Show-agenda (premium #8): tourdata/optredens per site.
+  db.exec(`
+    CREATE TABLE IF NOT EXISTS shows (
+      id TEXT PRIMARY KEY,
+      site_id TEXT NOT NULL,
+      date TEXT NOT NULL,
+      time TEXT,
+      city TEXT NOT NULL,
+      venue TEXT,
+      country TEXT,
+      ticket_url TEXT,
+      notes TEXT,
+      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+    );
+    CREATE INDEX IF NOT EXISTS idx_shows_site_date ON shows(site_id, date);
+  `);
+
   // Link-in-bio klikstatistiek (premium #6). Per (site, url) een teller; de
   // link-in-bio-pagina linkt via /links/go/:i dat de klik telt en doorstuurt.
Index: src/routes/admin-shows.js
===================================================================
--- src/routes/admin-shows.js	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
+++ src/routes/admin-shows.js	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
@@ -0,0 +1,91 @@
+/**
+ * Show-agenda (premium feature #8) — beheerkant.
+ *
+ *   GET  /admin/shows           -> lijst + toevoeg-formulier
+ *   POST /admin/shows           -> show toevoegen (optioneel notify-mail naar abonnees)
+ *   POST /admin/shows/:id/delete
+ *
+ * Premium + site-beheerder. Notify-mail vereist SMTP; zonder SMTP wordt de show
+ * gewoon opgeslagen (geen mail).
+ */
+
+import express from 'express';
+import db from '../config/database.js';
+import { v4 as uuid } from 'uuid';
+import { renderPage } from '../middleware/render.js';
+import { requireSiteManager } from '../middleware/auth.js';
+import { premiumUnlocked } from '../services/PatreonService.js';
+import { mailerConfigured, sendMail } from '../config/mailer.js';
+import { confirmedFor, counts } from '../services/SubscriberService.js';
+
+const router = express.Router();
+
+function esc(s) { return String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
+function fullUrl(req, p) {
+  const base = (process.env.PUBLIC_BASE_URL || ('https://' + (req.get('host') || ''))).replace(/\/$/, '');
+  return base + (req.res.locals.siteUrlBase || '') + p;
+}
+function premiumGate(req, res, next) {
+  if (!premiumUnlocked()) return res.status(403).send('Agenda is premium — koppel Patreon in Beheer → Instellingen.');
+  next();
+}
+function render(req, res, extra = {}) {
+  const site = res.locals.site;
+  const shows = db.prepare('SELECT * FROM shows WHERE site_id = ? ORDER BY date DESC, time DESC').all(site.id);
+  renderPage(req, res, 'pages/admin-shows', {
+    pageTitle: 'Agenda', bodyClass: 'on-admin',
+    shows, smtp: mailerConfigured(), notifyCount: confirmedFor(site.id, 'notify').length,
+    ...extra,
+  });
+}
+
+router.get('/', requireSiteManager, premiumGate, (req, res) => {
+  if (!res.locals.site) return res.status(404).send('Geen site.');
+  render(req, res);
+});
+
+router.post('/', requireSiteManager, premiumGate, async (req, res) => {
+  const site = res.locals.site;
+  if (!site) return res.status(404).send('Geen site.');
+  const b = req.body || {};
+  const date = (b.date || '').trim();
+  const city = (b.city || '').trim();
+  if (!date || !city) return render(req, res, { msg: 'Datum en plaats zijn verplicht.', msgKind: 'bad' });
+  let ticket = (b.ticket_url || '').trim();
+  if (ticket && !/^https?:\/\//i.test(ticket)) ticket = '';
+
+  db.prepare(`INSERT INTO shows (id, site_id, date, time, city, venue, country, ticket_url, notes)
+              VALUES (?,?,?,?,?,?,?,?,?)`).run(
+    uuid(), site.id, date, (b.time || '').trim() || null, city, (b.venue || '').trim() || null,
+    (b.country || '').trim() || null, ticket || null, (b.notes || '').trim() || null,
+  );
+
+  let sent = 0;
+  if (b.notify && mailerConfigured()) {
+    const subs = confirmedFor(site.id, 'notify');
+    const where = city + (b.venue ? ' — ' + b.venue : '');
+    for (const s of subs) {
+      const unsub = fullUrl(req, '/nieuwsbrief/uitschrijven/' + s.token);
+      try {
+        await sendMail({
+          to: s.email,
+          subject: 'Nieuwe show: ' + where + ' (' + date + ')',
+          text: (site.title || '') + ' speelt op ' + date + ' in ' + where + '.' + (ticket ? ('\nTickets: ' + ticket) : '') + '\n\nUitschrijven: ' + unsub,
+          html: '<p><strong>' + esc(site.title) + '</strong> speelt op <strong>' + esc(date) + '</strong> in ' + esc(where) + '.</p>' +
+                (ticket ? ('<p><a href="' + ticket + '">Tickets</a></p>') : '') +
+                '<p style="color:#888;font-size:12px"><a href="' + unsub + '">Uitschrijven</a></p>',
+        });
+        sent++;
+      } catch { /* sla over */ }
+    }
+  }
+  render(req, res, { msg: 'Show toegevoegd.' + (sent ? (' Notify gestuurd naar ' + sent + ' abonnee(s).') : ''), msgKind: 'ok' });
+});
+
+router.post('/:id/delete', requireSiteManager, premiumGate, (req, res) => {
+  const site = res.locals.site;
+  if (site) db.prepare('DELETE FROM shows WHERE id = ? AND site_id = ?').run(req.params.id, site.id);
+  res.redirect((res.locals.siteUrlBase || '') + '/admin/shows');
+});
+
+export default router;
Index: src/routes/shows.js
===================================================================
--- src/routes/shows.js	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
+++ src/routes/shows.js	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
@@ -0,0 +1,73 @@
+/**
+ * Show-agenda + notify-me (premium feature #8) — publieke kant.
+ *
+ *   GET  /shows         -> komende optredens + "houd me op de hoogte"-formulier
+ *   POST /shows/notify  -> aanmelden voor show-aankondigingen (subscribers, source
+ *                          'notify'; double opt-in als SMTP er is)
+ *
+ * De notify-bevestiging/uitschrijving hergebruikt de generieke subscriber-links
+ * (/nieuwsbrief/bevestigen|uitschrijven/:token). Hub: /user/:slug/shows.
+ */
+
+import express from 'express';
+import db from '../config/database.js';
+import { renderPage } from '../middleware/render.js';
+import { premiumUnlocked } from '../services/PatreonService.js';
+import { mailerConfigured, sendMail } from '../config/mailer.js';
+import { addSubscriber } from '../services/SubscriberService.js';
+
+const router = express.Router();
+
+function esc(s) { return String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
+function fullUrl(req, p) {
+  const base = (process.env.PUBLIC_BASE_URL || ('https://' + (req.get('host') || ''))).replace(/\/$/, '');
+  return base + (req.res.locals.siteUrlBase || '') + p;
+}
+function upcoming(siteId) {
+  const today = new Date().toISOString().slice(0, 10);
+  return db.prepare('SELECT * FROM shows WHERE site_id = ? AND date >= ? ORDER BY date ASC, time ASC').all(siteId, today);
+}
+
+router.get('/shows', (req, res, next) => {
+  if (!premiumUnlocked()) return next();
+  const site = res.locals.site;
+  if (!site) return next();
+  renderPage(req, res, 'pages/shows', {
+    pageTitle: 'Agenda — ' + (site.title || ''),
+    bodyClass: 'on-shows',
+    shows: upcoming(site.id),
+    notifyState: req.query.ok ? 'done' : (req.query.check ? 'check' : null),
+  });
+});
+
+router.post('/shows/notify', async (req, res, next) => {
+  if (!premiumUnlocked()) return next();
+  const site = res.locals.site;
+  if (!site) return next();
+  const email = (req.body.email || '').trim();
+  const doubleOptin = mailerConfigured();
+  const r = addSubscriber(site.id, email, 'notify', { doubleOptin });
+  if (!r.ok) {
+    return renderPage(req, res, 'pages/shows', {
+      pageTitle: 'Agenda', bodyClass: 'on-shows', shows: upcoming(site.id),
+      notifyState: 'error', notifyMsg: r.error === 'invalid_email' ? 'Controleer je e-mailadres.' : 'Er ging iets mis.',
+    });
+  }
+  if (r.status === 'pending') {
+    const link = fullUrl(req, '/nieuwsbrief/bevestigen/' + r.token);
+    const unsub = fullUrl(req, '/nieuwsbrief/uitschrijven/' + r.token);
+    try {
+      await sendMail({
+        to: email,
+        subject: 'Bevestig — show-updates van ' + (site.title || ''),
+        text: 'Bevestig dat je show-aankondigingen wilt ontvangen: ' + link + '\n\nUitschrijven: ' + unsub,
+        html: '<p>Bevestig dat je show-aankondigingen van <strong>' + esc(site.title) + '</strong> wilt ontvangen:</p>' +
+              '<p><a href="' + link + '">Bevestigen</a></p><p style="color:#888;font-size:12px"><a href="' + unsub + '">Uitschrijven</a></p>',
+      });
+    } catch { return res.redirect((res.locals.siteUrlBase || '') + '/shows'); }
+    return res.redirect((res.locals.siteUrlBase || '') + '/shows?check=1');
+  }
+  res.redirect((res.locals.siteUrlBase || '') + '/shows?ok=1');
+});
+
+export default router;
Index: src/server.js
===================================================================
--- src/server.js	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
+++ src/server.js	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
@@ -59,4 +59,6 @@
 import linkbioRoutes from './routes/linkbio.js';
 import embedRoutes from './routes/embed.js';
+import showsRoutes from './routes/shows.js';
+import adminShowsRoutes from './routes/admin-shows.js';
 
 if (!process.env.SESSION_SECRET) {
@@ -260,4 +262,5 @@
 app.use('/admin/stats', adminStatsRoutes);
 app.use('/admin/newsletter', adminNewsletterRoutes);
+app.use('/admin/shows', adminShowsRoutes);
 app.use('/admin', adminRoutes);
 app.use('/prutter', prutterRoutes);
@@ -279,4 +282,5 @@
 app.use('/', linkbioRoutes); // /links link-in-bio + klikstats (premium)
 app.use('/', embedRoutes); // /embed inbedbare audiospeler (premium)
+app.use('/', showsRoutes); // /shows agenda + notify-me (premium)
 app.use('/', postsRoutes);
 
Index: src/services/SubscriberService.js
===================================================================
--- src/services/SubscriberService.js	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
+++ src/services/SubscriberService.js	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
@@ -69,6 +69,10 @@
 }
 
-/** Bevestigde abonnees (email + token) voor een site — voor het versturen. */
-export function confirmedFor(siteId) {
+/** Bevestigde abonnees (email + token) voor een site — voor het versturen.
+ * Optioneel filteren op bron (bv. 'notify' voor show-aankondigingen). */
+export function confirmedFor(siteId, source) {
+  if (source) {
+    return db.prepare("SELECT email, token FROM subscribers WHERE site_id = ? AND status = 'confirmed' AND source = ?").all(siteId, source);
+  }
   return db.prepare("SELECT email, token FROM subscribers WHERE site_id = ? AND status = 'confirmed'").all(siteId);
 }
Index: src/views/pages/admin-shows.ejs
===================================================================
--- src/views/pages/admin-shows.ejs	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
+++ src/views/pages/admin-shows.ejs	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
@@ -0,0 +1,66 @@
+<%
+  var ss = (typeof shows !== 'undefined') ? shows : [];
+  var msg = (typeof msg !== 'undefined') ? msg : '';
+  var smtp = (typeof smtp !== 'undefined') ? smtp : false;
+  var nc = (typeof notifyCount !== 'undefined') ? notifyCount : 0;
+%>
+<section class="ash">
+  <p><a href="/admin" class="btn">&larr; Beheer</a></p>
+  <h1>Agenda</h1>
+
+  <% if (msg) { %><div class="ash-msg ash-<%= (typeof msgKind!=='undefined')?msgKind:'' %>"><%= msg %></div><% } %>
+
+  <p class="ash-note"><strong><%= nc %></strong> abonnee(s) voor show-aankondigingen.
+    <% if (!smtp) { %><br>⚠ SMTP niet ingesteld — shows worden opgeslagen, maar notify-mails kunnen pas verstuurd worden zodra je SMTP invult.<% } %></p>
+
+  <form method="POST" action="<%= siteUrlBase %>/admin/shows" class="ash-form">
+    <div class="ash-grid">
+      <label>Datum<input type="date" name="date" required></label>
+      <label>Tijd (optioneel)<input type="time" name="time"></label>
+      <label>Plaats<input type="text" name="city" required placeholder="Amsterdam"></label>
+      <label>Land (optioneel)<input type="text" name="country" placeholder="NL"></label>
+      <label class="ash-wide">Locatie/zaal (optioneel)<input type="text" name="venue" placeholder="Paradiso"></label>
+      <label class="ash-wide">Ticket-URL (optioneel)<input type="url" name="ticket_url" placeholder="https://..."></label>
+      <label class="ash-wide">Notitie (optioneel)<input type="text" name="notes" placeholder="Support: ..."></label>
+    </div>
+    <label class="ash-check"><input type="checkbox" name="notify" value="1" <%= smtp ? '' : 'disabled' %>> Abonnees per e-mail op de hoogte brengen<%= smtp ? '' : ' (SMTP vereist)' %></label>
+    <button type="submit" class="ash-add">+ Show toevoegen</button>
+  </form>
+
+  <% if (ss.length) { %>
+    <ul class="ash-list">
+      <% ss.forEach(function(s){ %>
+        <li class="ash-item">
+          <span class="ash-when"><%= s.date %><% if (s.time) { %> <%= s.time %><% } %></span>
+          <span class="ash-where"><%= s.city %><% if (s.venue) { %> — <%= s.venue %><% } %></span>
+          <form method="POST" action="<%= siteUrlBase %>/admin/shows/<%= s.id %>/delete" onsubmit="return confirm('Show verwijderen?')" style="margin:0">
+            <button type="submit" class="ash-del">🗑</button>
+          </form>
+        </li>
+      <% }); %>
+    </ul>
+  <% } else { %>
+    <p class="ash-empty">Nog geen shows.</p>
+  <% } %>
+</section>
+
+<style>
+  .ash { max-width: 680px; margin: 0 auto; padding: 24px 18px 64px; }
+  .ash h1 { margin: .4rem 0 14px; }
+  .ash-msg { padding: 10px 14px; border-radius: 10px; margin-bottom: 14px; }
+  .ash-ok { background: rgba(60,160,90,.15); } .ash-bad { background: rgba(200,60,60,.15); }
+  .ash-note { font-size: 13px; opacity: .8; margin: 0 0 18px; }
+  .ash-form { border: 1px solid rgba(128,128,128,.2); border-radius: 14px; padding: 16px; margin-bottom: 24px; }
+  .ash-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+  .ash-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 12.5px; font-weight: 600; }
+  .ash-grid .ash-wide { grid-column: 1 / -1; }
+  .ash-grid input { padding: 9px 11px; border-radius: 8px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font: inherit; font-weight: 400; }
+  .ash-check { display: flex; align-items: center; gap: 8px; margin: 12px 0; font-size: 13.5px; }
+  .ash-add { padding: 10px 18px; border-radius: 10px; border: none; background: var(--accent,#6b8f71); color: #fff; font-weight: 600; cursor: pointer; }
+  .ash-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
+  .ash-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border: 1px solid rgba(128,128,128,.18); border-radius: 10px; }
+  .ash-when { font-variant-numeric: tabular-nums; font-weight: 600; flex: 0 0 auto; }
+  .ash-where { flex: 1 1 auto; opacity: .85; }
+  .ash-del { background: none; border: none; cursor: pointer; font-size: 16px; opacity: .6; }
+  .ash-empty { opacity: .7; }
+</style>
Index: src/views/pages/admin.ejs
===================================================================
--- src/views/pages/admin.ejs	(revision b9dc94c5d408760545edee96aed16a6a387b1847)
+++ src/views/pages/admin.ejs	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
@@ -37,4 +37,5 @@
       <% if (tenancy !== 'hub' && primarySite) { %><a href="/downloads" class="btn" target="_blank">⬇ Downloads</a><% } %>
       <% if (tenancy !== 'hub' && primarySite) { %><a href="/links" class="btn" target="_blank">🔗 Link-in-bio</a><% } %>
+      <a href="/admin/shows" class="btn">📅 Agenda</a>
     <% } %>
     <a href="/admin/updates" class="btn">🔄 Updates</a>
Index: src/views/pages/shows.ejs
===================================================================
--- src/views/pages/shows.ejs	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
+++ src/views/pages/shows.ejs	(revision 8d32dcf6552ef4dcd02cb0465748921a55ac674b)
@@ -0,0 +1,65 @@
+<%
+  var ss = (typeof shows !== 'undefined') ? shows : [];
+  var st = (typeof notifyState !== 'undefined') ? notifyState : null;
+%>
+<section class="sh">
+  <h1 class="sh-h1">Agenda</h1>
+
+  <% if (st === 'done') { %><div class="sh-msg">✓ Je staat op de lijst — je hoort het zodra er een show wordt aangekondigd.</div>
+  <% } else if (st === 'check') { %><div class="sh-msg">✉ Check je mail om je aanmelding te bevestigen.</div>
+  <% } else if (st === 'error') { %><div class="sh-msg sh-bad"><%= (typeof notifyMsg!=='undefined')?notifyMsg:'Er ging iets mis.' %></div><% } %>
+
+  <% if (!ss.length) { %>
+    <p class="sh-empty">Geen aangekondigde shows op dit moment.</p>
+  <% } else { %>
+    <ul class="sh-list">
+      <% ss.forEach(function(s){ %>
+        <li class="sh-item">
+          <span class="sh-date">
+            <span class="sh-day"><%= (s.date||'').slice(8) %></span>
+            <span class="sh-mon"><%= (s.date||'').slice(5,7) %>/<%= (s.date||'').slice(0,4) %></span>
+          </span>
+          <span class="sh-meta">
+            <span class="sh-city"><%= s.city %><% if (s.country) { %>, <%= s.country %><% } %></span>
+            <% if (s.venue) { %><span class="sh-venue"><%= s.venue %><% if (s.time) { %> · <%= s.time %><% } %></span><% } else if (s.time) { %><span class="sh-venue"><%= s.time %></span><% } %>
+            <% if (s.notes) { %><span class="sh-notes"><%= s.notes %></span><% } %>
+          </span>
+          <% if (s.ticket_url) { %><a class="sh-tix" href="<%= s.ticket_url %>" target="_blank" rel="noopener">Tickets</a><% } %>
+        </li>
+      <% }); %>
+    </ul>
+  <% } %>
+
+  <div class="sh-notify">
+    <h2 class="sh-h2">Mis geen show</h2>
+    <p class="sh-sub">Laat je e-mail achter en je krijgt een seintje bij een nieuwe show. Uitschrijven kan altijd.</p>
+    <form method="POST" action="<%= siteUrlBase %>/shows/notify" class="sh-form">
+      <input type="email" name="email" required placeholder="jouw@email.nl" autocomplete="email">
+      <button type="submit" class="sh-btn">Houd me op de hoogte</button>
+    </form>
+  </div>
+</section>
+
+<style>
+  .sh { max-width: 640px; margin: 0 auto; padding: 32px 18px 64px; }
+  .sh-h1 { font-size: clamp(26px,5vw,38px); margin: 0 0 18px; }
+  .sh-msg { padding: 11px 14px; border-radius: 10px; background: rgba(60,160,90,.15); margin-bottom: 16px; }
+  .sh-bad { background: rgba(200,60,60,.15); }
+  .sh-list { list-style: none; margin: 0 0 36px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
+  .sh-item { display: flex; align-items: center; gap: 16px; padding: 12px 14px; border: 1px solid rgba(128,128,128,.2); border-radius: 12px; }
+  .sh-date { flex: 0 0 auto; text-align: center; min-width: 54px; }
+  .sh-day { display: block; font-size: 22px; font-weight: 700; line-height: 1; }
+  .sh-mon { font-size: 11px; opacity: .6; }
+  .sh-meta { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
+  .sh-city { font-weight: 600; }
+  .sh-venue { font-size: 13px; opacity: .7; }
+  .sh-notes { font-size: 12.5px; opacity: .6; margin-top: 2px; }
+  .sh-tix { flex: 0 0 auto; padding: 8px 14px; border-radius: 999px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; font-size: 13px; }
+  .sh-empty { opacity: .7; margin-bottom: 36px; }
+  .sh-notify { border-top: 1px solid rgba(128,128,128,.2); padding-top: 24px; }
+  .sh-h2 { margin: 0 0 6px; font-size: 18px; }
+  .sh-sub { opacity: .8; margin: 0 0 14px; }
+  .sh-form { display: flex; gap: 10px; flex-wrap: wrap; }
+  .sh-form input { flex: 1 1 200px; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font-size: 15px; }
+  .sh-btn { padding: 12px 18px; border-radius: 10px; border: none; background: var(--accent,#6b8f71); color: #fff; font-weight: 600; cursor: pointer; }
+</style>
