Neue Seite Verwaltung -> QR-Typen mit einem Schalter je Typ. Abgeschaltete Typen verschwinden vollstaendig von der Startseite und lassen sich nicht mehr neu anlegen. - Neue Tabelle QrTypeSetting; fehlender Eintrag heisst freigegeben, damit eine frische Installation ohne Vorbelegung laeuft und spaetere Typen automatisch erscheinen. - Registry-Feld 'active' heisst jetzt 'implemented' und beschreibt nur noch, ob der Typ ausprogrammiert ist. Ob er erscheint, entscheidet zusaetzlich die Freigabe aus der Datenbank (src/lib/qr/availability.ts). - Bestehende dynamische Codes bleiben von einer Abschaltung unberuehrt: die Weiterleitung funktioniert weiter und Bearbeiten bleibt moeglich. Ein gedruckter Code darf durch einen Schalter nicht ungueltig werden. - resolveQrRequest ist dadurch asynchron geworden; QrRequestError liegt jetzt in type-module.ts, sonst gaebe es einen Ringschluss mit availability.ts. - Startseite meldet jetzt auch den Fall, dass nur noch Platzhalter freigegeben sind - vorher standen dort 14 tote Kacheln ohne Erklaerung. - Kopfzeile: Schriftzug auf schmalen Geraeten ausgeblendet, sonst passte die dritte Navigationsschaltflaeche nicht mehr in die Zeile (7px Ueberlauf).
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.
Version 1 unterstützt den QR-Typ Website/URL, statisch wie dynamisch. Die übrigen 14 Typen sind bereits in der Oberfläche sichtbar und für Version 2 vorbereitet.
Inhalt
- Schnellstart
- Aktualisieren
- Konfiguration
- Bedienung
- Technischer Aufbau
- Entwicklung ohne Docker
- API
- Ausblick Version 2
Schnellstart
Voraussetzung: Docker mit Docker Compose.
cp .env.example .env # anschließend POSTGRES_PASSWORD und BASE_URL anpassen
docker compose up -d --build
Danach ist die Anwendung unter http://localhost:3000 erreichbar. Die Datenbankmigrationen laufen beim Start des App-Containers automatisch — es sind keine weiteren Schritte nötig.
Nützliche Befehle:
docker compose logs -f app # Protokoll der Anwendung
docker compose ps # Status beider Container
docker compose down # Stack stoppen (Daten bleiben erhalten)
docker compose down -v # Stack stoppen und Datenbank löschen
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 |
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 aller 15 QR-Typen. In Version 1 ist Website aktiv (Badge „Beliebt“); die übrigen Kacheln sind sichtbar, aber deaktiviert (Badge „Bald verfügbar“).
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.
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.
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.