Musikwünsche per QR-Code: Ein komplettes Gäste-Wunschsystem nur mit n8n

Wer als DJ auflegt, kennt das Spiel: Mitten im Übergang tippt dir jemand auf die Schulter und ruft dir einen Songwunsch ins Ohr. Drei Minuten später hast du ihn vergessen, der Gast ist beleidigt, und auf dem Mischpult stapeln sich Bierdeckel mit unleserlichen Titeln. Genau dieses Problem habe ich mir mit n8n vom Hals geschafft – und zwar komplett innerhalb von n8n, ohne externe Datenbank, ohne Google Sheets, ohne eigenes Hosting für ein Frontend.

Noch mal ein wenig Off-Topic: Ich persönlich liebe jedoch natürlich den offenen Austausch zwischen mir und den Gästen auf einer Party. Daher möchte ich mich als DJ hier nicht nur hinter einem QR-Code mit Songwünschen verstecken. Jedoch ergeben sich daraus auch neue Möglichkeiten das technische und das praktische zu Verbinden 🙂

Das Ergebnis: Die Gäste scannen einen QR-Code, landen auf einem schicken Webformular und schicken ihren Wunsch ab. Ich sehe alle Wünsche live auf einem dunklen, handy-tauglichen Dashboard hinter dem DJ-Pult, kann sie als „gespielt“ abhaken oder löschen. In diesem Artikel zeige ich Schritt für Schritt, wie das System aufgebaut ist – und welche Stolperfalle mich unterwegs erwischt hat.

Die Idee: n8n als Fullstack-Plattform

n8n ist eigentlich ein Workflow-Automatisierungstool, aber drei native Bausteine machen es zur kompletten Mini-Webanwendung:

Der Form Trigger liefert ein fertiges, gehostetes Webformular – das ist die Gäste-Seite hinter dem QR-Code. Niemand muss HTML schreiben oder einen Webserver betreiben.

Die Data Tables (verfügbar ab n8n 1.113) sind ein persistenter Key-Value-/Tabellenspeicher direkt in n8n. Sie ersetzen die Datenbank: Jeder Musikwunsch wird als Zeile gespeichert und überlebt Workflow-Neustarts.

Der Webhook-Node in Kombination mit Respond to Webhook kann beliebiges HTML ausliefern. Damit wird n8n kurzerhand zum Webserver für das DJ-Dashboard – inklusive Auto-Refresh und Aktions-Buttons.

Alles zusammen lebt in einem einzigen Workflow mit zwölf Nodes und drei Einstiegspunkten:

Voraussetzungen

Du brauchst eine n8n-Instanz ab Version 1.113 (Cloud oder self-hosted), die aus dem Partynetz erreichbar ist. Self-hosted heißt das konkret: eine öffentliche URL mit HTTPS, zum Beispiel über einen Reverse Proxy oder einen Tunnel wie Cloudflare Tunnel. Die Smartphones der Gäste müssen das Formular schließlich aufrufen können. Mehr ist es nicht – keine Datenbank, keine Credentials, keine externen APIs.

Schritt 1: Die Data Table anlegen

In der n8n-Oberfläche gibt es links den Punkt Data Tables. Dort lege ich eine Tabelle namens musikwuensche an, mit fünf Spalten vom Typ String:

SpalteInhalt
songSongtitel aus dem Formular
interpretInterpret aus dem Formular
gastName des Gasts (oder „Anonym“)
nachrichtOptionale Widmung an den DJ
statusneu, gespielt oder loeschen

Die Spalten id, createdAt und updatedAt legt n8n automatisch an – id brauchen wir später für die Buttons, createdAt für die Sortierung und die Uhrzeit im Dashboard.

Schritt 2: Das Gäste-Formular

Der erste Strang besteht aus genau zwei Nodes. Der Form Trigger bekommt den Pfad musikwunsch und vier Felder: Songtitel (Pflicht), Interpret (Pflicht), Name (optional) und eine Freitext-Nachricht an den DJ. Als Bestätigungstext nach dem Absenden habe ich „Danke! 🎶 Dein Wunsch ist beim DJ angekommen.“ hinterlegt – das Formular selbst rendert n8n fertig gestylt und mobiltauglich.

Dahinter hängt ein Data Table-Node mit der Operation Insert, der die Formularfelder auf die Tabellenspalten mappt. Zwei Kleinigkeiten lohnen sich hier: Der Name wird mit einem Fallback versehen, und jeder neue Wunsch startet mit dem Status neu.

song = {{ $json['Songtitel'] }}
interpret = {{ $json['Interpret'] }}
gast = {{ $json['Dein Name'] || 'Anonym' }}
nachricht = {{ $json['Nachricht an den DJ'] || '' }}
status = neu

Wichtig zu wissen: Der Form Trigger gibt die Feldwerte unter ihren Labels aus – deshalb die Zugriffe mit eckigen Klammern.

Sobald der Workflow aktiviert ist, erreichen die Gäste das Formular unter https://deine-instanz/form/musikwunsch. Aus dieser URL wird später der QR-Code.

Schritt 3: Das DJ-Dashboard

Hier wird es interessant, denn jetzt missbrauchen wir n8n als Webserver. Der zweite Strang beginnt mit einem Webhook-Node (Methode GET, Pfad dj-dashboard, Response-Modus Using Respond to Webhook Node).

Direkt dahinter sitzt ein IF-Node als Türsteher: Er vergleicht den Query-Parameter key mit einem Geheimwort. Das Dashboard ist schließlich eine öffentliche URL – ohne diesen simplen Schutz könnte jeder Gast, der die URL errät, die Wünsche anderer löschen. Ist der Key falsch, antwortet ein Respond-Node mit HTTP 403; ist er richtig, geht es weiter zum Data Table-Node mit der Operation Get, der alle Zeilen lädt. Hier unbedingt die Node-Option Always Output Data aktivieren – sonst bleibt der Workflow bei leerer Tabelle einfach stehen und der Browser wartet ewig auf eine Antwort.

Das Herzstück ist ein Code-Node, der aus den Zeilen eine komplette HTML-Seite baut. Die wichtigsten Zutaten:

// Daten holen, Soft-Deletes ausblenden, neueste zuerst
let rows = $input.all().map(i => i.json)
  .filter(r => r && r.song && r.status !== 'loeschen');
rows.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));

const offen = rows.filter(r => r.status !== 'gespielt');
const gespielt = rows.filter(r => r.status === 'gespielt');

// Eingaben escapen – Gäste-Input landet sonst ungefiltert im DJ-Browser!
const esc = s => String(s ?? '').replace(/[&<>"']/g,
  c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));

// Aktions-Buttons als relative Links auf den zweiten Webhook
const aktion = (r, a, label, cls) =>
  `<a class="btn ${cls}" href="dj-aktion?key=${encodeURIComponent(key)}` +
  `&id=${encodeURIComponent(r.id)}&aktion=${a}">${label}</a>`;

Drei Details daraus sind mir wichtig, weil sie über „funktioniert auf der Party“ entscheiden:

XSS nicht vergessen. Die Wünsche kommen von fremden Smartphones und landen im Browser des DJs. Ohne die esc()-Funktion könnte ein Scherzkeks <script>-Tags als Songtitel einreichen. Auf einer Hochzeit unwahrscheinlich? Vielleicht. Aber Escapen kostet drei Zeilen.

Relative Links. Die Buttons zeigen auf dj-aktion?... ohne Domain. Da Dashboard und Aktions-Webhook unter demselben Pfadpräfix /webhook/ liegen, löst der Browser das korrekt auf – der Workflow funktioniert dadurch auf jeder Instanz, ohne dass irgendwo eine URL hartkodiert ist.

Auto-Refresh per Meta-Tag. Ein schlichtes <meta http-equiv="refresh" content="20"> im Head lädt die Seite alle 20 Sekunden neu. Kein WebSocket, kein Polling-JavaScript – auf einer Party reicht das völlig, und es kann nicht kaputtgehen.

Dazu kommt etwas CSS im Dark Mode (hinterm Pult ist es dunkel, ein weißes Dashboard blendet) mit großen Touch-Flächen, und ein nettes Extra: Das Dashboard rekonstruiert aus den Request-Headern (host, x-forwarded-proto) die eigene Basis-URL und zeigt in einem ausklappbaren Bereich den QR-Code zum Gästeformular an – generiert über die freie API von qrserver.com. Den Code kann ich direkt vom Tablet abfotografieren lassen oder für den Aushang ausdrucken.

Den Abschluss bildet ein Respond to Webhook-Node, der das HTML mit dem Header Content-Type: text/html; charset=utf-8 ausliefert. Ohne diesen Header zeigt der Browser nur den Quelltext an.

Schritt 4: Die Aktionen – und warum Löschen hier „soft“ ist

Der dritte Strang verarbeitet die Button-Klicks: ein Webhook auf dj-aktion, wieder die Key-Prüfung, dann ein Data Table-Node mit der Operation Update. Der Trick: Der Node schreibt einfach den Query-Parameter aktion in die Status-Spalte der Zeile mit der übergebenen id:

Bedingung: id equals {{ $('DJ Aktion').first().json.query.id }}
Update: status = {{ $('DJ Aktion').first().json.query.aktion }}

Damit deckt ein einziger Node drei Buttons ab: „✓ Gespielt“ setzt gespielt, „↩ Zurück in die Liste“ setzt neu, und „✕ Löschen“ setzt loeschen. Gelöschte Wünsche werden also nicht physisch entfernt, sondern nur per Status markiert und vom Dashboard ausgefiltert – ein klassisches Soft Delete.

Nach der Aktion soll der DJ wieder auf dem Dashboard landen. Dafür antwortet der letzte Respond-Node mit einer Mini-HTML-Seite, die sofort zurück weiterleitet:

<meta http-equiv="refresh" content="0;url=dj-dashboard?key=...">

Auch hier wieder relativ, also instanzunabhängig.

Schritt 5: Aktivieren, QR-Code drucken, auflegen

Zum Schluss den Workflow auf Active schalten – ein Punkt, über den man leicht stolpert: Die Produktions-URLs (/form/... und /webhook/...) funktionieren nur bei aktiviertem Workflow. Im Editor-Testmodus gelten andere Test-URLs, die nach einem Aufruf wieder schließen.

Danach gibt es zwei URLs:

Gäste (QR-Code): https://deine-instanz/form/musikwunsch
DJ-Dashboard: https://deine-instanz/webhook/dj-dashboard?key=DEIN-GEHEIMER-KEY

Die Dashboard-URL lege ich mir als Lesezeichen auf dem Tablet ab, den QR-Code drucke ich auf Tischaufsteller. Fertig.

Grenzen und mögliche Ausbaustufen

Ehrlicherweise: Das System ist für eine Party gebaut, nicht für Stadiongrößen. Der Key in der URL ist Schutz gegen neugierige Gäste, keine echte Authentifizierung. Das Meta-Refresh lädt alle 20 Sekunden die ganze Seite neu statt elegant per Push zu aktualisieren. Und eine Duplikat-Erkennung („Layla wurde schon viermal gewünscht“) gibt es nicht – wobei sich genau das hübsch nachrüsten ließe: vor dem Insert per Get prüfen, ob die Kombination aus Song und Interpret schon existiert, und stattdessen einen Zähler hochsetzen. Auch denkbar: ein Voting, bei dem Gäste bestehende Wünsche upvoten, oder eine Anbindung an die Spotify-API, die zu jedem Wunsch automatisch BPM und Tonart nachschlägt.

Für mich zeigt das Projekt vor allem eines: Mit Form Trigger, Data Tables und Webhook-Responses deckt n8n inzwischen den kompletten Stack einer kleinen Webanwendung ab – Frontend, Backend und Datenhaltung in einem einzigen Workflow, aufgesetzt an einem Nachmittag. Und die Bierdeckel auf dem Mischpult? Die bleiben jetzt den Getränken vorbehalten.

Screenshots

Das Webformular für die Gäste:

Das DJ-Dashboard:

Schreibe einen Kommentar