Technische Dokumentation
Stand: 2026-11-05
Wie Wanderbibliothek gebaut ist: Architektur, Routen, Daten, Sicherheit und Betrieb. Gedacht für alle, die den Code lesen, die Anwendung betreiben oder prüfen wollen, was gespeichert wird.
Alles hier Beschriebene steht im Repository. Ändert sich der Code, ändert sich diese Seite mit.
Überblick
Modularer Monolith mit Next.js 16 (App Router mit Cache Components), strengem TypeScript und SQLite über better-sqlite3. Ein einziger Prozess: SQLite kennt nur einen Schreiber, und der Live-Bus für Hinweise liegt im Speicher.
Es gibt keine verpflichtenden Fremddienste. Cover und bibliografische Daten holt der Server und legt sie auf der Platte ab, sodass der Browser nie mit Dritten spricht.
Der Code steht unter der MIT-Lizenz. Diese Seite beschreibt den Code der Instanz, die sie ausliefert.
Architektur
src/
app/ Seiten und Route Handler (App Router)
api/ 71 Handler, nach Domäne gruppiert
styles/ Tokens, Typografie, Struktur, Karten, Dialoge, Detailseiten
proxy.ts CSP mit Nonce und Sicherheits-Header
components/ Server-Gerüst und interaktive Inseln
lib/
db/ Verbindung, Basisschema (v2), Migrationen (v3–v38), Seed
repo/ SQL-Abfragen je Domäne; `repo.ts` reexportiert nur
validation.ts Zod-Schemata mit übersetzten Meldungen
secret-box.ts umkehrbare Verschlüsselung mit Schlüsselring (TOTP, Versand)
e2e.ts Ende-zu-Ende-Verschlüsselung (ECDH + HKDF + AES-GCM)
route.ts Routen-Wrapper: Sitzung, Body, Kontingent, Fehler
tests/ 331 Unit- und Integrationstests (45 Dateien)
e2e/ 36 Browser-Tests mit Playwright
deploy/ systemd, nginx, Umgebungsvorlage und AnleitungDas Gerüst (site-shell.tsx, site-rail.tsx) ist ein Server Component: Seitenleiste, Kopfzeile, Fußzeile und mobile Navigation kommen fertig gerendert an, hydratisiert werden nur die interaktiven Inseln (account-menu, theme-toggle, locale-switcher, verify-email-banner).
Das deutsche Wörterbuch geht nicht an den Browser: i18n.ts wird nur auf dem Server importiert, und das Layout gibt dem Client-Provider nur die Texte der aktiven Sprache, nicht das ganze Wörterbuch. Sie sind außerdem über eine 32-Bit-Kennung (messageId) adressiert statt über den Quelltext: der Schlüssel ist zehn Ziffern lang und der spanische Text reist nicht mit, was das deutsche HTML von 236 auf 182 KB bringt.
getDb() hängt sich in prepare und verwendet kompilierte Anweisungen weiter (FIFO-Cache mit 400 Einträgen je Verbindung). Öffentliche Aggregate laufen über use cache mit dem Tag public-data, das die schreibenden Routen entwerten.
Das Rendering liest weder Date.now() noch new Date(): Next wertet das beim Vorab-Rendern als instabilen Wert. Zeitstempel für Hinweise, Wartung und Limits setzt SQLite mit strftime('%Y-%m-%dT%H:%M:%fZ','now').
Routen
Die internen Seiten verlangen eine Sitzung; ohne sie leiten sie auf /?auth=1&next=… um. Öffentlich sind nur die Startseite, die Rechtstexte, die Abläufe mit eigenem Link und diese Dokumentation. robots.txt sperrt Katalog und Profile, damit Name und Stadt der Leser:innen nicht in Suchmaschinen landen.
| Route | Inhalt |
|---|---|
/ | Öffentliche Startseite: Hero, vier Kennzahlen und eine Reihe Bücher. |
/descubrir | Vollständiger Katalog mit Suche, Kategorien, Sortierung und Seitenzahl in der URL. Braucht eine Sitzung. |
/ofrecer | Ein Exemplar einstellen: die ISBN füllt die bibliografischen Daten. |
/buscar | Globale Suche: Werke, Clubs, Leser:innen und Listen. |
/estanteria, /intercambios | Private Bereiche: eigene Exemplare und Anfragen mit dem nächsten Schritt. |
/comunidad | Hub mit Tabs (Gespräche, Aktivität, Clubs, Leser:innen, Listen) und Umfang über ?scope=. |
/lectores | Verzeichnis mit Suche, Stadt und Sortierung nach Nähe oder Aktivität. |
/clubes, /clubes/[clubId] | Clubs je Stadt: Anfragen, laufende Lektüre, Abstimmung, Teilnahme und privates Forum. |
/listas, /listas/[listId] | Öffentliche Leselisten, nur von der eigenen Person bearbeitbar. |
/libro/[id] | Exemplarseite mit Metadaten und JSON-LD Book/Offer. |
/obra/[bookId] | Kanonisches Werk: Gespräch zum Titel, Exemplare und Empfehlungen. |
/u/[id] | Öffentliches Profil: Biografie, Kennzahlen, Rezensionen, Regal und Folgen. |
/cuenta | Einstellungen: Profil, Hinweise, Passwort, Verschlüsselung, zweiter Faktor, Export und Löschung. |
/admin | Moderation der Community; verlangt einen aktiven zweiten Faktor. |
/privacidad, /aviso-legal | Datenschutzerklärung (Art. 13 DSGVO) und Impressum (§ 5 DDG). |
/verificar, /restablecer | E-Mail bestätigen und neues Passwort über den Link setzen. |
/documentacion | Diese Seite. |
/api/health | Zustand von Dienst und Datenbank. |
/sitemap.xml, /robots.txt | Indexierung: die Sitemap veröffentlicht nur die Startseite. |
Die API besteht aus 71 Handlern unter src/app/api. Fast alle werden mit route() (src/lib/route.ts) deklariert, das an einer Stelle Sitzung, Zod-Schema des Bodys, Kontingent mit Herkunftsprüfung und handleApiError() anwendet; tests/route.test.ts prüft an den Dateien, dass keine Route Fehler unübersetzt durchlässt.
/api/auth/*: Registrierung, Anmeldung, Abmeldung, Sitzung, Bestätigung und Zurücksetzen mit Einmal-Tokens./api/listings,/api/listings/[id],…/favorite: Exemplare einstellen, bearbeiten, zurückziehen und merken./api/requests,/api/requests/[id]und Ableger (eventsper SSE,messages,read,review,handover,handover/shipping,handover/verify): der ganze Tauschablauf./api/books/*: Werksuche, Kommentare, Likes, Hinweise je Werk und Lesestände./api/clubs/*: Club, Mitglieder, Moderator:innen, Einladungen, Teilnahme, Übergabe, Forum, Abstimmungen und Beitrittsanfragen./api/lists/*,/api/direct/*,/api/users/[userId]/follow,/api/blocks: Listen, verschlüsselte Direktnachrichten, Folgen und Blockieren./api/account/*: Profil, Sprache, Passwort, Verschlüsselungsschlüssel, TOTP, Export und Löschung./api/admin/*,/api/reports,/api/comments/[id],/api/notifications: Meldungen, Moderation, Kommentare und Hinweise./api/cover/*,/api/isbn/[isbn]: zwischengespeicherte Cover und bibliografische Daten (DNB und Open Library)./api/cron/maintenance: geplante Aufgaben, mitMAINTENANCE_TOKEN.
Daten
booksist die Ausgabe (eindeutige ISBN),listingsdas konkrete Exemplar;books.work_keybündelt Ausgaben desselben Werks, sodass sie Gespräch und Exemplarliste teilen.- Zustände eines Exemplars:
available,reserved,given,paused,removed. - Zustände einer Anfrage:
pending,accepted,declined,cancelled,completed,expired. - Suche über FTS5 (
listings_fts), von Triggern gepflegt;unaccent()faltet Akzente, damit „Lucía“ auch als „lucia“ gefunden wird. citiesenthält 123 Städte in Deutschland,city_distancesdie vorberechnete Distanzmatrix.
Migrationen werden über PRAGMA user_version versioniert (aktuell: v38) und erhalten die Daten einer bestehenden Installation. Die v38 öffnet den Katalog wieder für ganz Deutschland, ergänzt requests.ship_kind und ship_street, verschlüsselt die Versanddaten, die die v37 im Klartext abgelegt hat, und entfernt die Deutsche-Post-Filialen; die v36 brachte die verschlüsselten Direktnachrichten, die v37 den Versand per Post.
Versanddaten (ship_number, ship_station, ship_street, ship_postal_code, ship_city) liegen verschlüsselt mit APP_ENCRYPTION_KEY, und jeder Wert ist über AAD an seine Anfrage und seine Spalte gebunden (ship:<anfrage>:<feld>): ein in eine andere Zeile verschobenes Chiffrat lässt sich nicht öffnen. Entschlüsselt wird nur in der Ansicht der beiden Tauschpartner:innen, und Hinweise wiederholen das Ziel nicht.
Nachrichten eines Tauschs und Direktnachrichten werden im Browser verschlüsselt (ECDH P-256 + HKDF-SHA256 + AES-GCM-256 mit AAD). Der Server speichert den Umschlag und den Schlüssel-Fingerabdruck und lehnt Klartext ab, sobald eine der beiden Seiten einen Schlüssel veröffentlicht hat (409).
- Ein Exemplar wird nur reserviert, wenn es noch
availableist – innerhalb derselben Transaktion. - Beim Annehmen wandern konkurrierende Anfragen nach
expired; annehmen oder ablehnen kann nur die Eigentümer:in. - Der Abschluss ist beidseitig: jede Seite bestätigt getrennt mit dem 6-stelligen Code, und erst die zweite Bestätigung schließt den Tausch.
- Unbeantwortete Anfragen verfallen nach 14 Tagen und benachrichtigen die wartende Person.
- Blockierungen, gesperrte Konten und pausierte Exemplare werden in SQL gefiltert, nicht im Client.
Sicherheit
- Passwörter mit asynchronem
scryptund eigenem Salt; die Anmeldung prüft immer einen Hash (gegen einen Köder, wenn es das Konto nicht gibt), sodass die Antwortzeit nicht verrät, welche Adressen registriert sind. - Sitzung mit 256 Bit im
httpOnly-Cookie undSameSite=Lax; in Produktion wird nur das Präfix__Host-akzeptiert, und in der Datenbank liegt nur ihr SHA-256. - Zweiter Faktor (TOTP nach RFC 6238, Fenster ±1 Schritt, Anti-Replay) ist für Moderation in Produktion Pflicht; die Geheimnisse liegen verschlüsselt.
- Atomare Kontingente in der Datenbank je IP und je Konto, mit
429undRetry-After. - Alle schreibenden Zugriffe laufen über
guard()(Herkunft und Kontingent), undreadJson()verlangtapplication/jsonund kappt Bodys über 64 KB. - CSP mit Nonce und
strict-dynamic,X-Frame-Options: DENY, COOP/CORP,nosniff,Referrer-Policy,Permissions-Policyund HSTS in Produktion (src/proxy.ts). - Verzeichnis
0700und Datenbank0600;npm run db:backupnutzt die.backup()-API von SQLite und verschlüsselt mit AES-256-GCM, wennWANDERBIBLIOTHEK_BACKUP_KEYgesetzt ist. - Registrierung mit Honeypot, Mindestzeit und optionalem Cloudflare Turnstile; die Antwort ist allgemein gehalten, um keine Adressen zu verraten.
- Konto-Tokens sind einmalig, gehasht und befristet; ein Passwortwechsel beendet alle Sitzungen.
GET /api/account/exportliefert alle Daten als JSON, und das Löschen des Kontos entfernt eigene Inhalte, ohne fremde Gespräche zu zerreißen.
Umgebungsvariablen
Ohne APP_ENCRYPTION_KEY wird kein Geheimnis im Klartext abgelegt: das Anlegen scheitert mit einem Konfigurationsfehler. In Produktion prüft src/instrumentation.ts die Konfiguration beim Start und schreibt den Grund ins Log.
| Variable | Standard | Zweck |
|---|---|---|
WANDERBIBLIOTHEK_DB | .data/wanderbibliothek.db | Pfad der SQLite-Datenbank (:memory: in Tests). |
NEXT_PUBLIC_SITE_URL | http://localhost:3000 | Basis für sitemap.xml, robots.txt und Canonicals. In Produktion Pflicht. |
APP_ENCRYPTION_KEY | nicht gesetzt | 32 Byte in Hex oder Base64 für TOTP und Versanddaten. Pflicht. |
APP_ENCRYPTION_KEY_ID | abgeleitet | Lesbarer Name des aktiven Schlüssels; fehlt er, dient ein Präfix seines SHA-256. |
APP_ENCRYPTION_OLD_KEYS | leer | Frühere Schlüssel nur zum Lesen, solange eine Rotation läuft. |
WANDERBIBLIOTHEK_ALLOW_PLAINTEXT | leer | Nur Entwicklung: legt Geheimnisse im Klartext ab, wenn kein Schlüssel da ist. |
WANDERBIBLIOTHEK_BACKUP_KEY | nicht gesetzt | AES-256-GCM-Schlüssel der Sicherungen. |
WANDERBIBLIOTHEK_BACKUP_KEEP | 14 | Anzahl der Sicherungen, die beim Rotieren bleiben. |
WANDERBIBLIOTHEK_BACKUP_HOOK | nicht gesetzt | Befehl, der BACKUP_FILE für eine externe Kopie erhält. |
WANDERBIBLIOTHEK_COVER_CACHE | neben der Datenbank | Ordner für geladene und hochgeladene Cover. |
WANDERBIBLIOTHEK_SKIP_ENV_CHECK | nicht gesetzt | Überspringt die Umgebungsprüfung beim Start. |
MAIL_WEBHOOK_URL | nicht gesetzt | Empfängt {to, subject, text}; Versand läuft über mail_outbox mit Wiederholungen. |
MAIL_WEBHOOK_TOKEN | nicht gesetzt | Bearer-Authentifizierung des Mail-Webhooks. |
MAINTENANCE_TOKEN | nicht gesetzt | Token für POST /api/cron/maintenance; ohne ihn 404. |
AUDIT_RETENTION_DAYS | 0 | Tage, die admin_audit aufbewahrt wird (0 = dauerhaft). |
ADMIN_EMAIL / ADMIN_PASSWORD | nicht gesetzt | Legen das erste Verwaltungskonto an, wenn keines existiert. |
REQUIRE_EMAIL_VERIFICATION | nicht gesetzt | 1 verlangt eine bestätigte Adresse zum Einstellen, Anfragen, Kommentieren und Schreiben. |
NEXT_PUBLIC_TURNSTILE_SITE_KEY / TURNSTILE_SECRET | nicht gesetzt | Aktivieren Cloudflare Turnstile bei der Registrierung. |
ERROR_WEBHOOK_URL / ERROR_WEBHOOK_TOKEN | nicht gesetzt | Empfangen 500er als JSON (Sentry, Logtail oder eigener Webhook). |
TRUST_PROXY | nicht gesetzt | 1 nur mit eigenem Proxy davor: erlaubt X-Forwarded-For/-Host und aktiviert Kontingente je IP. |
LEGAL_CONTROLLER / LEGAL_ADDRESS / LEGAL_CONTACT_EMAIL | nicht gesetzt | Füllen /privacidad und /aviso-legal. |
LEGAL_VAT_ID / LEGAL_PHONE / LEGAL_REGISTER | nicht gesetzt | Optionale Angaben für das Impressum. |
SEED_DEMO | 1 außerhalb der Produktion | Legt Demodaten an; in Produktion nur mit ausdrücklicher 1. |
GOOGLE_BOOKS_API_KEY | nicht gesetzt | Erhöht das Kontingent von Google Books für Cover. |
Tests und Continuous Integration
npm test führt 331 Unit- und Integrationstests in 45 Dateien aus (Node-Testrunner auf SQLite im Speicher), und npm run test:e2e ergänzt 36 Browser-Tests mit Playwright, die ihren eigenen Server auf Port 3100 starten.
npm run test:coverage setzt Mindestschwellen von 92 % Zeilen, 80 % Zweigen und 88 % Funktionen. Die Continuous Integration (.github/workflows/ci.yml) führt lint, typecheck, test, build, npm audit --omit=dev, die Browser-Tests und einen Build des Docker-Images ohne Veröffentlichung aus.
npm install
npm run lint && npm run typecheck && npm test
npm run build
npx playwright install chromium && npm run test:e2eBetrieb
npm run db:backupnutzt die.backup()-API von SQLite, prüft die Integrität, rotiert die Sicherungen, verschlüsselt bei vorhandenem Schlüssel und startetWANDERBIBLIOTHEK_BACKUP_HOOK. Die Datei nie bei aktivem WAL im laufenden Betrieb kopieren.POST /api/cron/maintenancemitAuthorization: Bearer $MAINTENANCE_TOKENlässt Anfragen verfallen, räumt Sitzungen, Hinweise und Kontingente auf und verschickt die Erinnerungen der Clubs.npm run user:verify -- <adresse>undnpm run admin:2fa-off -- <adresse>sind die Notausgänge, wenn Mail oder zweiter Faktor jemanden aussperren.- Jede Antwort trägt
x-request-idundx-response-time, und jede Anfrage hinterlässt eine Zeile[req] {…}im Log. Diereferenceeines 500ers sind die ersten 8 Zeichen dieser ID. - Nur eine Instanz: SQLite hat einen einzigen Schreiber, und der Hinweis-Bus liegt im Speicher. Skalierung verlangt PostgreSQL und Redis/Pubsub.
- Gemessene Obergrenze mit
npm run bench:sqlite: rund 99 000 Einzel-Schreibvorgänge/s, 250 000/s im Stapel, 350 000 Lesezugriffe/s über den Primärschlüssel und 2 000/s bei einem Filter ohne Index über 5 000 Zeilen. Die echte Grenze ist die Dauer jeder Transaktion, nicht die Zahl der Aufrufe.
Geheimnisse liegen als enc:v2:<schlüssel>:<flags>:<iv>.<tag>.<daten> vor. Schlüsselwechsel ohne Datenverlust:
# 1. Probelauf: der neue Schlüssel ist aktiv, der alte bleibt zum Lesen
APP_ENCRYPTION_KEY=<neu> APP_ENCRYPTION_OLD_KEYS=<alt> \
npm run rotate:secrets -- --dry-run
# 2. Wenn der Bericht passt, ohne --dry-run
APP_ENCRYPTION_KEY=<neu> APP_ENCRYPTION_OLD_KEYS=<alt> \
npm run rotate:secrets
# 3. APP_ENCRYPTION_OLD_KEYS entfernenDeployment
- Die vollständige Anleitung (Systembenutzer, systemd, nginx, TLS, Timer und Wiederherstellung) steht in
deploy/README.md. deploy/wanderbibliothek.service,deploy/nginx.confunddeploy/env.example: die Anwendung lauscht auf127.0.0.1:3000, nginx macht das TLS, und/api/requests/läuft ohne Buffering, damit der SSE-Kanal nicht abreißt.deploy/wanderbibliothek-maintenance.timer(alle 15 Minuten) unddeploy/wanderbibliothek-backup.timer(täglich).- Alternativ
Dockerfileunddocker-compose.yml, mehrstufig und nur mit Produktionsabhängigkeiten; der Image-Build wird in der CI geprüft. - Fehlt etwas Wichtiges in der Konfiguration, startet der Prozess zwar, antwortet aber auf alles mit 500 und dem Grund im Log, statt funktionsfähig zu wirken.
tsxist eine Produktionsabhängigkeit:admin:create,db:backup,user:verifyundsmokebrauchen es, auch im Image mitnpm ci --omit=dev.
Oberfläche und Barrierefreiheit
- Ein einziges Gerüst (
SiteShell) für die internen Seiten; die Startseite ist die Ausnahme und überlässt dem Katalog die volle Breite. - Farben kommen aus Tokens (
src/app/styles/tokens.css): außerhalb dieser Datei gibt es keine Literale, deshalb teilen helles und dunkles Thema dasselbe Markup. - Vier Breakpoints (480, 768, 1120 und 1400/1800 px), Abstandsstaffel von 4 bis 48 px und Bedienelemente mit 40 px, mobil 44 px.
- Dialoge mit gefangenem Fokus, Schließen per
Escapeund Fokus-Rückgabe; Sprunglink „Zum Inhalt“,aria-currentin der Navigation, Filter mitaria-pressedund Fehler mitrole="alert". - Mindestkontrast AA, Schrift ab 12 px, Touch-Ziele mit 44 px auf dem Handy und
prefers-reduced-motionwird respektiert. - Die Oberfläche ist zweisprachig mit Deutsch als Standard: Spanisch kommt über den Umschalter, der das Cookie
wanderbibliothek-localesetzt und mit Sitzung die Kontoeinstellung für Hinweise und E-Mails speichert.
Weiterlesen
- RepositoryQuellcode, Verlauf und private Meldungen zu Sicherheitslücken.
- DOCUMENTATION.mdDieselbe Seite auf Englisch, im Repository.
- CONTRIBUTING.mdUmgebung, Qualitätsbefehle, Commit-Konventionen und Migrationsregeln.
- SECURITY.mdWie man eine Schwachstelle meldet und was beim Selbsthosten zu beachten ist.
- deploy/README.mdSchrittweises Deployment mit systemd, nginx und TLS.
- LICENSEMIT-Lizenz.
- NOTICEHerkunftsnachweise für das Titelbild und die Schrift der Cover.