Ein datenschutzfreundlicher Lebenslauf-Editor mit Next.js 14 und KI (technische Fallstudie)
Inhalt
Die meisten Lebenslauf-Editoren folgen demselben Muster: Sie tippen, die App schickt alles an einen Server, und Ihr Lebenslauf liegt in einer fremden Datenbank. Wir sind von einer anderen Vorgabe ausgegangen: Lebensläufe von Nutzerinnen und Nutzern werden nie auf unseren Servern gespeichert. Dieser Beitrag erklärt die technischen Entscheidungen, die daraus folgen. Und wenn Sie einfach nur einen Lebenslauf brauchen: Hier geht es zum Editor.
Das Problem des klassischen Modells
Client (React)
↓
API
↓
Datenbank
↓
Lebensläufe liegen auf dem Server
- Datenschutzrisiko: Namen, Telefonnummern, E-Mail-Adressen und Werdegänge an einem Ort sind ein attraktives Ziel.
- Compliance-Aufwand: Wer personenbezogene Daten speichert, braucht Löschkonzepte, muss Auskunfts- und Löschanfragen nach DSGVO bearbeiten und alles dokumentieren.
- Vertrauen: „Wir speichern sicher“ ist ein Versprechen. „Wir speichern nicht“ lässt sich überprüfen.
Architektur: Local-First
Der Lebenslauf lebt auf dem Gerät des Nutzers. Der Server übernimmt nur, was im Browser nicht geht, und behält die Eingaben nicht.
Browser
Zustand-Store (Arbeitskopie)
↓ entprellte, zusammenführende Schreibvorgänge
localStorage (alle Lebensläufe des Nutzers)
↓ optional, vom Nutzer ausgelöst
Eigenes Google Drive des Nutzers (Scope drive.file)
Server (zustandslos für Lebenslaufdaten)
/api/pdf → rendert ein PDF im Arbeitsspeicher und gibt es zurück
/api/ai/* → leitet den ausgewählten Text an den KI-Anbieter weiter
Zielkonflikte und unser Umgang damit:
- Browserdaten löschen heißt Lebensläufe löschen → optionale Sicherung im eigenen Drive und ein Hinweis vor dem Download.
- localStorage ist auf wenige Megabyte begrenzt → Profilfotos werden vor dem Speichern im Browser komprimiert.
- Keine automatische Synchronisierung zwischen Geräten → ebenfalls die Drive-Sicherung, vollständig unter Kontrolle des Nutzers.
Technologie
Next.js 14 (App Router)
Inhaltsseiten (Vorlagen, Ratgeber, Blog) werden für Suchmaschinen serverseitig gerendert. Der Editor selbst ist eine clientseitige Anwendung. Route Handler stellen die wenigen benötigten Serverfunktionen bereit.
State: Zustand mit persist
Redux wäre überdimensioniert gewesen. Ein einziger Zustand-Store hält alle Lebensläufe und die Arbeitskopie des aktiven; die persist-Middleware schreibt in den localStorage.
// vereinfacht
const useResumeStore = create(
persist(
(set, get) => ({
resumes: {}, // id → gespeicherter Lebenslauf
activeResumeId: null,
resumeData: emptyResume, // Arbeitskopie des aktiven Lebenslaufs
updatePersonalInfo: (patch) => sync(set, get, patch),
}),
{ name: 'resume-storage', version: CURRENT_STORE_VERSION, migrate },
),
)
Zwei Details waren wichtiger als erwartet:
- Versionierte Migrationen. Ändert sich die Struktur eines Lebenslaufs, wird die Store-Version erhöht und ein Migrationsschritt wandelt alte Daten um. Sonst verlieren wiederkehrende Nutzer beim nächsten Besuch ihre Lebensläufe. Jede Migration hat einen Unit-Test.
- Ein zusammenführender Speicheradapter. Mit zwei offenen Tabs gewinnt bei naiver Persistenz der letzte Schreibvorgang, und die Änderungen des anderen Tabs gehen stillschweigend verloren. Unser Adapter entprellt Schreibvorgänge, führt sie mit dem bereits Gespeicherten zusammen, hält Löschungen als Tombstones mit Ablaufdatum fest und überschreibt keine Daten, die eine neuere App-Version geschrieben hat. Schreibfehler (etwa ein voller Speicher) erscheinen in einer Speicheranzeige, statt still zu scheitern.
PDF-Erzeugung
Wir wollten PDFs mit echtem, markierbarem Text (damit Bewerbermanagementsysteme sie auslesen können) und pixelgenauem Layout. Clientseitige Canvas-Ansätze erzeugen Bilder von Text und scheitern an der ersten Anforderung.
Die Lösung: Der Browser schickt das HTML der Vorschau an einen Route Handler, der es per Playwright mit Headless-Chromium rendert und das PDF zurückgibt. Nichts wird auf einen Datenträger geschrieben.
// vereinfacht
const page = await browser.newPage()
await page.setContent(html, { waitUntil: 'networkidle' })
const pdf = await page.pdf({ format: 'A4', printBackground: true })
await page.close()
return new Response(pdf, { headers: { 'Content-Type': 'application/pdf' } })
Die eigentliche Herausforderung war das mehrseitige Layout. Seitenränder und Umbruchregeln existieren zweimal: im Druck-CSS der PDF-Route und in der Live-Vorschau, die Seitenumbrüche simuliert, damit man sieht, wo eine Seite endet. Ein Unit-Test hält beides synchron, und ein Skript prüft Seitenformat und Seitenzahl für jede Vorlage gegen einen laufenden Server.
KI-Integration
Die KI-Funktionen (Formulierungen verbessern, Kenntnisse vorschlagen, Kurzprofil oder Anschreiben entwerfen, Keywords mit einer Stellenanzeige abgleichen) laufen über Groq mit dem Vercel AI SDK. Für jede KI-Route gelten feste Regeln:
- Ein einziger Ort für den Modellnamen. Anbieter nehmen Modelle vom Markt; ein in mehreren Routen fest verdrahteter Modellname kann alle KI-Funktionen auf einmal lahmlegen. Das Modell wird an genau einer Stelle konfiguriert.
- Nichts erfinden. Jeder Prompt enthält eine gemeinsame Regel, die erfundene Zahlen, Daten, Arbeitgeber oder Erfolge verbietet.
- Begrenzte Ein- und Ausgabe. Anfragen haben eine Größenbeschränkung, Nutzertext wird vor dem Prompt gekürzt, Ausgabetokens sind gedeckelt.
- Sicheres Logging. SDK-Fehler können den Request-Body enthalten, hier also den Lebenslauftext. Fehler werden nur als bereinigte Zusammenfassung protokolliert.
- Ehrliches Streaming. Streaming-Antworten reichen Anbieterfehler an den Client weiter, damit ein Fehler nicht als leere „erfolgreiche“ Antwort erscheint.
- Rate-Limiting in der Middleware, da die KI-Routen kein Konto voraussetzen.
Diese Routen verarbeiten den empfangenen Text und behalten nichts.
Sicherheitsgrundlagen
- Bereinigter Rich Text: CMS-Inhalte (Blogbeiträge, Seiten) werden beim Speichern mit DOMPurify bereinigt, mit einer expliziten Liste erlaubter Tags und Attribute.
- Content Security Policy pro Route: Nur Inhaltsseiten, auf denen Werbung erscheinen darf, erlauben Werbe-Hosts; Editor und alle übrigen Seiten blockieren sie.
- Admin-Sitzungen sind signierte JWTs in httpOnly-Cookies, werden in der Middleware geprüft und lassen sich mit TOTP-Zwei-Faktor-Authentifizierung absichern.
Was wir gelernt haben
1. Local-First passt zu diesem Anwendungsfall
Ein Lebenslauf ist ein kleines, persönliches Dokument, das eine Person bearbeitet. Das passt natürlich zu Local-First. Viele Anwendungen, die alles zentral speichern, bräuchten das eigentlich nicht.
2. Die Persistenz ist der schwierige Teil
Mehrere Tabs, Schemaänderungen, Speichergrenzen: Der eigentliche Aufwand steckte darin, den lokalen Speicher zuverlässig zu machen, nicht in der Oberfläche.
3. Datenschutz muss überprüfbar sein
Jeder kann den Netzwerk-Tab öffnen und sehen, was der Editor sendet. Das überzeugt mehr als jede Werbeaussage.
4. KI braucht Leitplanken, nicht nur Prompts
Ein Erfindungsverbot, Eingabegrenzen, sicheres Logging und eine zentrale Modellkonfiguration verhindern die unangenehmsten Fehler.
5. Veröffentlichen, dann verbessern
Funktionen wie die Drive-Sicherung und mehrseitige PDFs kamen, nachdem die reale Nutzung gezeigt hatte, dass sie gebraucht werden.
Fazit
„Wir schützen Ihre Daten“ ist ein Versprechen. „Wir haben Ihre Daten nicht“ ist eine Architektur. Wer ein datenschutzfreundliches Produkt baut, kommt mit Local-First-State, zustandslosen Serverfunktionen und überprüfbarem Verhalten sehr weit. Probieren Sie den Lebenslauf-Editor aus, jetzt, da Sie wissen, was dahintersteckt.
Passende Ratgeber und Quellen
- Datenschutzerklärung: was Cvbuildly wo und wie lange verarbeitet
- Über uns: warum wir das Tool lokal zuerst gebaut haben
- Next.js-Dokumentation: das Framework, auf dem die App aufbaut (Englisch)
- MDN: Window.localStorage: wie der lokale Speicher des Browsers funktioniert (Englisch)
- Google Drive API: Scopes: warum der Scope drive.file nur von der App erstellte Dateien erreicht (Englisch)
Erstellen Sie jetzt Ihren Lebenslauf
Kostenlos und ohne Registrierung; Ihre Daten bleiben ausschließlich in Ihrem Browser.