Social Media buendelt Profile auf einer Seite. Wer „@firma" eintippt, meint das Profil beim gewaehlten Netzwerk — daraus wird die volle Adresse. Statt nachgezeichneter Markenlogos steht ein Kreis in der Hausfarbe mit Monogramm; ein schlecht nachgebautes Logo sieht schlechter aus als ein sauberes Zeichen. Video erkennt YouTube, Vimeo und direkte Dateien. Der Rahmen zum Anbieter laedt erst auf Klick, und davor steht, dass dabei Daten uebertragen werden — diese Seite bekommen Fremde nach einem Scan zu sehen, ungefragt einen Drittanbieter einzubinden waere nicht in Ordnung. Videodateien laden mit preload="none": wer unterwegs scannt, zahlt das Volumen.
QR-Code-Generator
Eine Webanwendung zum Erzeugen von QR-Codes — als Docker-Compose-Stack aus zwei Containern: der Next.js-Anwendung und einer PostgreSQL-Datenbank.
Umgesetzt sind 9 der 15 QR-Typen. Die übrigen sechs sind in der Oberfläche sichtbar, als Platzhalter markiert und lassen sich über die Verwaltung ausblenden.
Inhalt
- Schritt für Schritt zum laufenden Stack
- Hinter einem Reverse Proxy (Nginx Proxy Manager)
- Wenn etwas nicht läuft
- Aktualisieren
- Konfiguration
- Bedienung
- Anmeldung, Gruppen und Rollen
- Technischer Aufbau
- Entwicklung ohne Docker
- API
- Ausblick Version 2
Schritt für Schritt zum laufenden Stack
Vom leeren Server bis zum ersten QR-Code. Alle Befehle laufen auf dem Rechner, auf dem die Anwendung später läuft.
Schritt 1 — Voraussetzungen prüfen
Gebraucht werden Docker mit Compose v2 und Git:
docker compose version # erwartet: Docker Compose version v2.x oder höher
git --version
Kommt docker: command not found, fehlt Docker — die
offizielle Anleitung installiert
Engine und Compose in einem Rutsch.
Schritt 2 — Projekt holen
git clone https://git.wilde-server.de/florian.wilde/qrcode-generator.git
cd qrcode-generator
Schritt 3 — Konfiguration anlegen
cp .env.example .env
nano .env
Zwei Werte musst du anfassen:
| Wert | Was eintragen |
|---|---|
POSTGRES_PASSWORD |
Ein eigenes Passwort. Es steht nur in dieser Datei und wird nirgends sonst gebraucht. |
DATABASE_URL |
Dasselbe Passwort noch einmal — es steckt mitten im Verbindungsstring. |
Und einen solltest du bewusst setzen:
| Wert | Was eintragen |
|---|---|
BASE_URL |
Die Adresse, unter der die Anwendung von außen erreichbar ist. |
Beispiel für ein eigenes Passwort MeinGeheimes123:
POSTGRES_PASSWORD=MeinGeheimes123
DATABASE_URL=postgresql://qrcode:MeinGeheimes123@db:5432/qrcode?schema=public
BASE_URL=http://192.168.1.50:3000
Warum
BASE_URLso wichtig ist: Ein dynamischer QR-Code enthält nicht das eigentliche Ziel, sondern die Kurz-URL{BASE_URL}/r/{slug}. Steht dortlocalhost, funktioniert der Code nur auf dem Server selbst — ein Handy läuft ins Leere. Trage deshalb die LAN-Adresse des Servers oder die öffentliche Domain ein. Bereits gedruckte Codes lassen sich später nicht mehr umbiegen, also am besten vor dem ersten produktiven Code festlegen.
Der Hostname db im DATABASE_URL bleibt unverändert — das ist der
Servicename aus der docker-compose.yml, nicht dein Server.
Schritt 4 — Starten
docker compose up -d --build
Der erste Durchlauf baut das Image und dauert je nach Maschine 5 bis 15 Minuten. Spätere Starts brauchen nur wenige Sekunden.
Schritt 5 — Prüfen, ob alles läuft
docker compose ps
Erwartet werden zwei Container, db mit dem Zusatz (healthy):
NAME SERVICE STATUS
qrcode-generator-app-1 app Up 30 seconds
qrcode-generator-db-1 db Up 45 seconds (healthy)
Und im Protokoll:
docker compose logs app
→ Datenbankmigrationen werden angewendet …
All migrations have been successfully applied.
→ Anwendung wird gestartet …
▲ Next.js 16.3.0
✓ Ready
Die Datenbank richtet sich beim ersten Start selbst ein — es gibt keinen Extraschritt dafür.
Schritt 6 — Aufrufen
Im Browser die Adresse öffnen, die du als BASE_URL eingetragen hast, zum
Beispiel http://192.168.1.50:3000. Es erscheint „Was möchtest du
erstellen?“ mit den Typ-Kacheln.
Schritt 7 — Ersten Code erstellen
- Kachel Website anklicken.
- Adresse eingeben, etwa
https://example.com— die Vorschau erscheint sofort. - PNG oder SVG herunterladen. Fertig.
Für einen Code, dessen Ziel du später ändern kannst:
- Unter „Art des Codes“ auf Dynamisch wechseln.
- Optional einen Namen vergeben, damit du ihn wiederfindest.
- Dynamischen Code speichern — erst dabei entsteht die Kurz-URL, deshalb ist der Download bis dahin gesperrt.
- Danach herunterladen. Unter Meine Codes lässt sich das Ziel jederzeit ändern, ohne den gedruckten Code anzufassen.
Schritt 8 — Aufräumen, was du nicht brauchst
Unter Verwaltung → QR-Typen kannst du einzelne Typen ausblenden — etwa die sechs, die noch nicht umgesetzt sind, damit sie gar nicht erst auftauchen.
Die wichtigsten Befehle im Betrieb
docker compose logs -f app # Protokoll mitlesen (Abbruch mit Strg+C)
docker compose ps # Status beider Container
docker compose restart app # Anwendung neu starten
docker compose down # Stack stoppen — die Daten bleiben erhalten
docker compose up -d # wieder starten
docker compose down -v # Stack stoppen UND die Datenbank löschen
Hinter einem Reverse Proxy (Nginx Proxy Manager)
Soll die Anwendung unter einer eigenen Domain mit HTTPS laufen, übernimmt der Proxy die Zertifikate; die Anwendung selbst bleibt unverändert.
1. Port nur lokal veröffentlichen. Wenn Proxy und Anwendung auf demselben
Rechner laufen, muss Port 3000 nicht nach außen offen sein. In der .env:
APP_PORT=3000
BASE_URL=https://qr.deine-domain.de
2. Im Nginx Proxy Manager einen Proxy Host anlegen:
| Feld | Wert |
|---|---|
| Domain Names | qr.deine-domain.de |
| Scheme | http |
| Forward Hostname / IP | IP des Docker-Hosts, z. B. 192.168.1.50 |
| Forward Port | 3000 |
| Websockets Support | an |
| Block Common Exploits | an |
| SSL | Zertifikat anfordern, Force SSL aktivieren |
Läuft der Proxy als Container im selben Docker-Netz wie dieser Stack, kann
statt der IP auch der Servicename app mit Port 3000 stehen — dann muss
das Port-Mapping gar nicht nach außen offen sein.
3. BASE_URL auf die öffentliche Adresse setzen — mit https:// und ohne
Port. Danach:
docker compose up -d
BASE_URLmuss zur Adresse passen, unter der Besucher die Anwendung aufrufen. Steht dort weiterhinhttp://192.168.1.50:3000, zeigen neue dynamische Codes ins interne Netz statt auf die Domain.
Mehrere Domains
Sollen Kurz-URLs unter verschiedenen Adressen laufen — etwa je Kunde eine —, lege sie unter Verwaltung → Domains an und richte im Proxy für jede einen eigenen Proxy Host auf dieselbe Anwendung ein.
Die Weiterleitung erkennt am Host-Header, welche Domain gemeint ist, und
prüft ihn gegen die registrierten Domains. Ein unbekannter Hostname endet mit
404 — über einen gefälschten Header lässt sich also keine fremde Domain
ansprechen.
Zwei Dinge, die daraus folgen:
- Der Slug ist je Domain eindeutig, nicht global. Derselbe sprechende Slug darf auf zwei Domains liegen und zeigt dort auf verschiedene Ziele.
- Eine Domain kann mehreren Gruppen dienen. Das heißt aber nicht, dass diese die Codes der jeweils anderen sehen — die Übersicht bleibt strikt nach Gruppe gefiltert.
BASE_URL bleibt dabei wichtig: sie liefert die Standard-Domain für Aufrufe
über eine nicht registrierte Adresse (etwa direkt über die IP im LAN) und
bestimmt, ob Kurz-URLs mit http oder https gebildet werden.
Wenn etwas nicht läuft
POSTGRES_PASSWORD fehlt — bitte .env aus .env.example anlegen
Die .env fehlt oder ist leer. Schritt 3 nachholen.
ports are not available: ... bind: address already in use
Port 3000 ist belegt. Entweder den anderen Dienst beenden oder in der .env
einen anderen Port setzen, z. B. APP_PORT=3100, dann docker compose up -d.
Der Container db bleibt unhealthy oder startet immer neu
Meist ein Datenverzeichnis aus einer älteren PostgreSQL-Version. Protokoll
ansehen mit docker compose logs db. Wenn dort von einem inkompatiblen
Datenlayout die Rede ist und du die Daten nicht brauchst:
docker compose down -v — Achtung, das löscht die Datenbank.
Der QR-Code führt auf dem Handy ins Leere
BASE_URL zeigt auf localhost. Auf die LAN-Adresse oder Domain ändern,
docker compose up -d, und die betroffenen dynamischen Codes neu
herunterladen. Statische Codes sind davon nicht betroffen.
Die Anwendung antwortet nicht, docker compose ps zeigt sie aber als Up
docker compose logs app ansehen. Kommt dort ein Datenbankfehler, stimmt
meist das Passwort in DATABASE_URL nicht mit POSTGRES_PASSWORD überein —
beide Werte müssen identisch sein.
Nach einem Update fehlt etwas oder die Seite ist leer
docker compose up -d --build erzwingt einen Neubau. Hilft das nicht:
docker compose down && docker compose up -d --build.
Aktualisieren
Für neue Versionen liegt ein Update-Skript bei — einmal für Linux/macOS und einmal für Windows. Es holt den aktuellen Stand aus Git, baut den Stack neu und wartet, bis die Anwendung wieder antwortet.
./update.sh # Linux / macOS
.\update.ps1 # Windows
Beim ersten Aufruf unter Linux muss das Skript einmal ausführbar gemacht
werden: chmod +x update.sh.
Was das Skript tut:
- prüft, ob
git,docker composeund die.envvorhanden sind, - bricht ab, wenn es lokale Änderungen gibt — sonst würde
git pullim Konflikt enden, - holt den neuen Stand mit
git merge --ff-onlyund zeigt die neuen Commits, - baut den Stack nur neu, wenn es tatsächlich Änderungen gab,
- wartet, bis die Anwendung auf
APP_PORTantwortet, und zeigt sonst das Protokoll, - gibt die Migrationszeilen aus dem Start-Protokoll und den Containerstatus aus.
Optionen:
| Option | Wirkung |
|---|---|
--force / -Force |
Auch ohne neue Commits neu bauen |
--prune / -Prune |
Danach verwaiste Docker-Images entfernen (docker image prune) |
--help |
Hilfe anzeigen |
Die Datenbank bleibt beim Update erhalten — neue Migrationen wendet der App-Container beim Start automatisch an. Wer lieber von Hand aktualisiert:
git pull
docker compose up -d --build
Konfiguration
Alle Einstellungen stehen in der .env im Projektverzeichnis (Vorlage:
.env.example).
| Variable | Bedeutung | Standard |
|---|---|---|
BASE_URL |
Öffentliche Adresse der App — landet in dynamischen QR-Codes | http://localhost:3000 |
APP_PORT |
Host-Port, auf dem die App veröffentlicht wird | 3000 |
AUTH_ENABLED |
Anmeldung mit Benutzern, Gruppen und Rollen | false |
DATABASE_URL |
Verbindungsstring der App zur Datenbank | — |
POSTGRES_USER |
Datenbankbenutzer | qrcode |
POSTGRES_PASSWORD |
Datenbankpasswort (muss gesetzt sein) | — |
POSTGRES_DB |
Name der Datenbank | qrcode |
Wichtig: BASE_URL richtig setzen
Ein dynamischer QR-Code enthält nicht das eigentliche Ziel, sondern die
Kurz-URL {BASE_URL}/r/{slug}. Diese Adresse muss von den Geräten erreichbar
sein, die den Code später scannen.
- Nur am selben Rechner testen →
http://localhost:3000genügt. - Im LAN mit dem Handy scannen → die LAN-Adresse des Servers eintragen, z. B.
http://192.168.1.50:3000.
Wird BASE_URL nachträglich geändert, zeigen bereits gedruckte dynamische
Codes weiterhin auf die alte Adresse. Am besten also vor dem ersten
produktiven Code festlegen.
Nach einer Änderung an der .env:
docker compose up -d
Ändern sich POSTGRES_*-Werte, muss DATABASE_URL entsprechend mitgezogen
werden — der Hostname darin bleibt db (der Servicename aus
docker-compose.yml).
Bedienung
1. Typ wählen
Die Startseite zeigt unter „Was möchtest du erstellen?“ ein Kachel-Grid der freigegebenen QR-Typen. Umgesetzte Typen sind anklickbar, die übrigen tragen das Badge „Bald verfügbar“.
| Typ | Kodiert als | Statisch | Dynamisch |
|---|---|---|---|
| Website | die URL selbst | ✓ | ✓ |
mailto: mit Betreff und Text |
✓ | ✓ | |
| SMS | SMSTO: |
✓ | ✓ |
https://wa.me/… |
✓ | ✓ | |
| Standort | geo: — dynamisch über OpenStreetMap |
✓ | ✓ |
| vCard | vCard 3.0 | ✓ | — |
| WLAN | WIFI: |
✓ | — |
| App-Link | Weiterleitung je nach Gerät | — | ✓ |
| Zahlung | GiroCode (EPC069-12) oder Zahlungslink | ✓ | nur Link |
Warum manche Typen nur eine Betriebsart können:
- vCard und WLAN werden vom Gerät direkt ausgewertet — die Kamera legt einen Kontakt an oder richtet das Netz ein. Über eine Kurz-URL würde stattdessen nur ein Browser aufgehen.
- App-Link kann nicht statisch sein: welches Ziel richtig ist, steht erst beim Scannen fest. Die Weiterleitung wertet den User-Agent aus und schickt iOS in den App Store, Android zu Google Play und alles Übrige auf die Ausweichadresse.
- GiroCode ist kein Web-Ziel, sondern ein Datensatz für Banking-Apps. Als dynamischer Code funktioniert daher nur die Variante „Zahlungslink“ — die Anwendung weist das beim Speichern ausdrücklich ab, statt einen Code entstehen zu lassen, der beim Scannen ins Leere führt.
Zum WLAN-Code: Das Passwort steht im Klartext im Code — wer ihn scannt oder abfotografiert, kennt es. Für Gästenetze ist das gewollt. Besteht ein Netzwerkname oder Passwort ausschließlich aus Hex-Ziffern (etwa
abc123), setzt die Anwendung ihn wie in der Spezifikation vorgesehen in Anführungszeichen — sonst würden manche Geräte den Wert als Hexzahl deuten.
2. Statisch oder dynamisch
| Statisch | Dynamisch | |
|---|---|---|
| Im Code kodiert | die Ziel-URL selbst | die Kurz-URL {BASE_URL}/r/{slug} |
| Ziel später änderbar | nein | ja, jederzeit |
| Braucht diesen Server | nein | ja — er leitet weiter |
| Scans werden gezählt | nein | ja |
| Wird gespeichert | nein | ja, in PostgreSQL |
Ein dynamischer Code muss einmal gespeichert werden, denn erst dabei entsteht seine Kurz-URL. Bis dahin zeigt die Vorschau einen Platzhalter und der Download ist gesperrt — so kann kein Code entstehen, der ins Leere führt.
3. Gestalten
- Farben: Vorder- und Hintergrund per Farbwähler oder als Hex-Wert. Fünf Voreinstellungen und ein Knopf zum Tauschen der Farben stehen bereit.
- Kontrastwarnung: Die Anwendung berechnet das Kontrastverhältnis und warnt bei schlecht scanbaren Kombinationen — ebenso bei hellem Vordergrund auf dunklem Grund, den viele Scanner-Apps nicht erkennen.
- Größe: 256 bis 2048 px (gilt für das PNG; das SVG skaliert verlustfrei).
- Fehlerkorrektur: L, M (Standard), Q oder H. Höhere Level machen den Code robuster gegen Beschädigung, aber auch dichter.
- Rand: Breite der Ruhezone in Modulen. Weniger als 4 kann das Scannen erschweren.
Die Vorschau wird bei jeder Änderung serverseitig neu erzeugt — sie zeigt also exakt das, was auch heruntergeladen wird.
4. Herunterladen
PNG für Druck und Office-Dokumente, SVG für verlustfreie Skalierung (Plakate, Schilder, Druckvorstufe).
5. Verwaltung: welche Typen erscheinen
Unter Verwaltung → QR-Typen lässt sich jeder der 15 Typen einzeln ein- und ausblenden. Ausgeblendete Typen verschwinden vollständig von der Startseite und lassen sich nicht mehr neu anlegen — praktisch, um die noch nicht umgesetzten Typen gar nicht erst zu zeigen.
Zwei Eigenschaften sind dabei zu unterscheiden:
| Bedeutung | Woher | |
|---|---|---|
| umgesetzt | Der Typ ist ausprogrammiert. In Version 1 gilt das nur für „Website“. | Registry im Code |
| freigegeben | Die Verwaltung zeigt den Typ an. | Datenbank, jederzeit änderbar |
Nur umgesetzt und freigegeben heißt benutzbar. Freigegeben, aber nicht umgesetzt, erscheint als Kachel „Bald verfügbar“.
Bereits gespeicherte Codes bleiben unberührt. Schaltet man einen Typ ab, leiten vorhandene dynamische Codes weiter wie bisher und lassen sich weiter bearbeiten. Ein gedruckter Code wird durch das Abschalten seines Typs also nicht ungültig — gesperrt ist nur das Anlegen neuer Codes.
Ohne Eintrag in der Datenbank gilt ein Typ als freigegeben. Eine frische Installation zeigt also alles, und Typen aus späteren Versionen erscheinen automatisch.
6. Meine Codes
Unter „Meine Codes“ stehen alle dynamischen Codes mit Vorschaubild, Kurz-URL, aktuellem Ziel, Scan-Anzahl, Erstellungsdatum und letztem Scan. Dort lässt sich
- das Ziel ändern — die Kurz-URL und damit der gedruckte Code bleiben gültig,
- der Code löschen (mit Bestätigungsdialog),
- das Bild erneut als PNG oder SVG herunterladen.
Liste oder Karten. Voreingestellt ist die Liste — eine Zeile je Code, gedacht für viele Projekte nebeneinander. Über die beiden Schalter rechts neben der Sortierung wechselst du zur Kartenansicht mit großer Vorschau. Die Wahl merkt sich der Browser.
Dazu kommen
- ein Suchfeld über Name, Ziel, Kurz-URL und Typ,
- eine Sortierung nach Datum, Name oder Scan-Anzahl,
- ein Gruppenfilter, sobald Codes in mehr als einer Gruppe liegen.
Auf schmalen Bildschirmen blendet die Liste die hinteren Spalten aus; die Kurz-URL rückt dann unter den Namen. Zum Herunterladen dient dort die Kartenansicht.
Statische Codes erscheinen hier nicht, weil sie nicht gespeichert werden.
Version 1 ist für den Einzelplatz-/LAN-Betrieb gedacht und hat keine Benutzerverwaltung. Wer die Anwendung erreicht, sieht und ändert alle Codes. Die App gehört daher nicht ungeschützt ins offene Internet.
Anmeldung, Gruppen und Rollen
Standardmäßig ist die Anmeldung aus: wer die Anwendung erreicht, darf alles. Für den Einzelplatz- oder LAN-Betrieb ist das gewollt. Sobald mehrere Leute damit arbeiten, lässt sie sich einschalten.
Einschalten
In der .env:
AUTH_ENABLED=true
Danach:
docker compose up -d
Beim nächsten Aufruf erscheint die Ersteinrichtung und du legst den ersten Administrator an. Diese Seite schließt sich, sobald es einen Benutzer gibt — danach führt sie zur normalen Anmeldung.
Fehlt die Zeile in deiner
.env? Die.envwird vongit pullnicht angefasst — sie gehört bewusst nicht ins Repository. Wurde sie vor Einführung der Anmeldung angelegt, fehltAUTH_ENABLEDdarin. Einfach ergänzen; ohne die Zeile giltfalse.
Ob es angekommen ist, zeigt:
docker compose exec app env | grep AUTH_ENABLED
Aussperren ist nicht möglich. Der Schalter steht bewusst in der
.envund nicht in der Oberfläche. Wer den Zugang verliert, setztAUTH_ENABLED=false, startet neu und kommt wieder an alles heran.
Bestehende Codes gehen dabei nicht verloren: die Migration legt eine Gruppe Standard an und ordnet ihnen diese zu.
Die vier Ebenen
| Ebene | Darf |
|---|---|
| Instanz-Administrator | alles: Benutzer, Gruppen, QR-Typen und sämtliche Codes |
| Administrator einer Gruppe | alles in seiner Gruppe, einschließlich Mitglieder und Rollen |
| Bearbeiter | Codes der Gruppe anlegen, ändern, löschen |
| Betrachter | Codes der Gruppe ansehen und herunterladen — sonst nichts |
Ein Benutzer kann in mehreren Gruppen sein, mit unterschiedlicher Rolle je Gruppe. Er sieht ausschließlich die Codes der Gruppen, in denen er Mitglied ist.
| Aktion | Betrachter | Bearbeiter | Admin der Gruppe | Instanz-Admin |
|---|---|---|---|---|
| Codes sehen, Bilder herunterladen | ✓ | ✓ | ✓ | ✓ |
| Statische Codes erzeugen | ✓ | ✓ | ✓ | ✓ |
| Dynamische Codes anlegen und ändern | – | ✓ | ✓ | ✓ |
| Codes löschen | – | ✓ | ✓ | ✓ |
| Mitglieder und Rollen der Gruppe | – | – | ✓ | ✓ |
| Gruppen und Benutzer anlegen | – | – | – | ✓ |
| QR-Typen freigeben | – | – | – | ✓ |
Was unabhängig von der Anmeldung bleibt
Die Weiterleitung unter /r/{slug} ist immer öffentlich — sonst könnte
niemand einen gedruckten Code scannen. Sie verrät nur das Ziel, nicht die
Verwaltung.
Benutzer verwalten
Unter Verwaltung → Benutzer: Konten anlegen, Passwörter setzen, Konten deaktivieren oder löschen.
- Passwörter werden mit scrypt gespeichert (Node-Standardbibliothek, kein natives Modul — dadurch keine Abhängigkeit, die zur Container-Plattform passen muss).
- Ein neues Passwort oder das Deaktivieren beendet alle offenen Sitzungen des Betroffenen sofort.
- Nach acht Fehlversuchen ist ein Konto 15 Minuten gesperrt; ein Administrator kann die Sperre vorzeitig aufheben.
- Der letzte aktive Administrator kann sich weder selbst deaktivieren noch sich die Rechte entziehen noch gelöscht werden.
- Passwörter kann nur ein Administrator setzen. Einen Versand per E-Mail gibt es bewusst nicht — das spart einen SMTP-Zugang.
Gruppen verwalten
Unter Verwaltung → Gruppen: Gruppen anlegen und Mitglieder mit Rollen zuweisen. Eine Gruppe, die noch Codes enthält, lässt sich nicht löschen — sonst führten gedruckte Codes ins Leere.
Technischer Aufbau
Stack
| Baustein | Verwendet |
|---|---|
| Framework | Next.js 16 (App Router, TypeScript), Frontend + API |
| Oberfläche | React 19, Tailwind CSS 4 |
| Datenbank | PostgreSQL 18 |
| ORM | Prisma 7 mit @prisma/adapter-pg |
| QR-Erzeugung | qrcode, serverseitig, Ausgabe als PNG und SVG |
| Validierung | Zod 4, im Browser und noch einmal auf dem Server |
Der Stack läuft ohne Reverse Proxy: Der App-Container wird direkt per Port-Mapping veröffentlicht.
Verzeichnisse
prisma/
schema.prisma Datenmodell (Modell QrCode)
migrations/ Versionierte Migrationen
docker/
entrypoint.sh Migration + Serverstart im Container
update.sh / update.ps1 Update-Skript (git pull + Neubau + Prüfung)
src/
app/
page.tsx Startseite mit dem Kachel-Grid
erstellen/[type]/ Generator je QR-Typ
meine-codes/ Übersicht der dynamischen Codes
r/[slug]/route.ts Weiterleitung dynamischer Codes (302)
api/
qr/preview/ Live-Vorschau (SVG als Data-URL)
qr/download/ Download als PNG oder SVG
codes/ Anlegen und Auflisten dynamischer Codes
codes/[id]/ Ändern und Löschen
codes/[id]/qr/ Bild eines gespeicherten Codes
components/
qr-forms/ Formulare je QR-Typ + deren Registry
qr-generator.tsx Generator-Oberfläche
codes-list.tsx „Meine Codes“
design-panel.tsx Farben, Größe, Fehlerkorrektur, Rand
lib/
qr/
type-module.ts Vertrag eines QR-Typs (defineQrType)
registry.ts Registry aller 15 Typen
types/website.ts Der einzige aktive Typ in V1
design.ts Gestaltungsoptionen + Zod-Schema
contrast.ts Kontrastbewertung
render.ts PNG-/SVG-Erzeugung
request.ts Anfrage-Schema und Auflösung des Code-Inhalts
db.ts Prisma-Client
env.ts Laufzeit-Konfiguration
Datenmodell
Nur dynamische Codes werden gespeichert. Das Modell QrCode hält neben Slug,
Zähler und Gestaltung ein generisches data-Feld (JSONB) sowie den Typ — so
brauchen die Typen aus Version 2 keine Schemaänderung.
Wie ein QR-Typ aufgebaut ist
Jeder Typ ist ein Modul, das über defineQrType erzeugt wird und vier Dinge
beisteuert: Metadaten (Titel, Beschreibung, Icon), ein Zod-Schema, die
Kodierung des Inhalts und eine Kurzfassung für Listen.
export const websiteType = defineQrType({
id: "website",
title: "Website",
description: "Verlinke eine beliebige Webadresse.",
icon: "globe",
active: true,
supportsDynamic: true,
schema: websiteDataSchema,
defaultData: { url: "" },
encode: (data) => data.url,
summary: (data) => describe(data.url),
});
Innerhalb des Moduls arbeitet man mit dem echten, aus dem Zod-Schema abgeleiteten Datentyp; nach außen zeigt die Registry eine einheitliche Schnittstelle. Deshalb kennen Vorschau, Download, Speicherung und Weiterleitung keinen einzigen typspezifischen Sonderfall.
Warum 302 und kein 301?
Die Weiterleitung unter /r/{slug} antwortet mit 302 (temporär) und
Cache-Control: no-store. Ein permanenter Redirect würde in Browsern hängen
bleiben: Zieländerungen kämen nicht an und der Scan-Zähler bliebe stehen.
Entwicklung ohne Docker
Voraussetzung: Node.js 20.9+ und eine erreichbare PostgreSQL-Instanz.
npm install
# Datenbank z. B. als Einzelcontainer starten
docker run -d --name qr-db -p 5432:5432 \
-e POSTGRES_USER=qrcode -e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=qrcode \
postgres:18-alpine
# .env anlegen und DATABASE_URL auf localhost zeigen lassen:
# DATABASE_URL=postgresql://qrcode:devpass@localhost:5432/qrcode?schema=public
npm run db:migrate # Migrationen anwenden und Prisma Client erzeugen
npm run dev # http://localhost:3000
Weitere Skripte:
npm run build # Produktionsbuild
npm run typecheck # TypeScript prüfen
npm run lint # ESLint
npm run db:studio # Prisma Studio (Datenbank ansehen)
Hinweis zu npm 11: Installationsskripte sind standardmäßig blockiert. Die Pakete, die welche ausführen dürfen (Prisma lädt darüber seine Engine-Binaries), stehen im Feld
allowScriptsderpackage.json.
API
Alle Endpunkte antworten im Fehlerfall mit { "error": "…" } und einer
deutschen Meldung.
| Methode | Pfad | Zweck |
|---|---|---|
POST |
/api/qr/preview |
Vorschau als SVG-Data-URL |
POST |
/api/qr/download |
PNG- oder SVG-Datei |
GET |
/api/codes |
Alle dynamischen Codes |
POST |
/api/codes |
Dynamischen Code anlegen |
GET |
/api/codes/{id} |
Einzelnen Code lesen |
PATCH |
/api/codes/{id} |
Ziel, Name oder Gestaltung ändern |
DELETE |
/api/codes/{id} |
Code löschen |
GET |
/api/codes/{id}/qr |
Bild des Codes (?format=png|svg&download=1) |
GET |
/r/{slug} |
Weiterleitung, erhöht den Scan-Zähler |
GET |
/api/admin/types |
Zustand aller QR-Typen |
PATCH |
/api/admin/types |
Typ freigeben oder abschalten |
Beispiel:
curl -X POST http://localhost:3000/api/codes \
-H 'Content-Type: application/json' \
-d '{
"type": "website",
"data": { "url": "https://example.com" },
"design": { "fgColor": "#0F172A", "bgColor": "#FFFFFF",
"size": 512, "ecLevel": "M", "margin": 4 },
"label": "Plakat Eingang"
}'
Ausblick Version 2
Geplant sind die übrigen 14 Typen: vCard, App-Link, Mehrere Links, Social Media, WhatsApp, PDF, Bild, Video, E-Mail, SMS, Standort, Zahlung, WLAN und Feedback.
Die Architektur ist darauf ausgelegt — ein neuer Typ besteht aus:
- einem Modul in
src/lib/qr/types/(Zod-Schema, Kodierung, Kurzfassung), - einer Formular-Komponente in
src/components/qr-forms/, - je einem Eintrag in
src/lib/qr/registry.ts(ersetzt dort dendefineComingSoonType-Platzhalter) und insrc/components/qr-forms/index.ts.
Am bestehenden Code aus Version 1 ist dafür nichts zu ändern: Vorschau, Download, Gestaltung, Speicherung dynamischer Codes und die Weiterleitung arbeiten für alle Typen gleich.
Typen, die statt einer Weiterleitung eine eigene Landingpage brauchen (etwa
„Mehrere Links“ oder „Feedback“), setzen dafür redirectTarget auf eine
interne Adresse — auch dafür ist keine Änderung am Datenmodell nötig, weil
data als JSONB gespeichert wird.