Jede Karte adaptiv. Jeder Streak erarbeitet.
Vollständige Lernplattform: FSRS-Spaced-Repetition, AI-Karteikarten und -Quizze aus PDFs/Notizen, Gamification mit XP/Streaks/Quests/Cosmetics, vier Stripe-Tiers und ein Klassenzimmer-Modus für Lehrer. Next.js 16 App Router · Prisma 6 · Postgres · Redis · Stripe · OpenAI · Soketi. Self-hosted via Coolify auf dediziertem Hetzner-Server hinter Cloudflare-Edge.

Warum es das gibt.
Der etablierte Spaced-Repetition-Markt hat zwei Pole. Anki ist algorithmisch ehrlich — SM-2/FSRS, harte Defaults, jahrzehntelang gewachsen — aber das UI fühlt sich an wie Forschungssoftware aus 2008, und die Gamification ist bestenfalls nicht vorhanden. Quizlet hat den Look, aber der Lernalgorithmus ist eine glorifizierte Reihenfolge — kein FSRS, keine Memory-Stability-Modellierung, kein Anspruch, dich beim Vergessen zu erwischen.
Flashbuddy schließt die Lücke: ein FSRS-Lernkern, der wissenschaftlich an Anki vorbeiläuft, eingebettet in einen Duolingo-artigen Gamification-Layer (XP, Streaks, Streak-Freezes, tägliche Quests, freischaltbare Cosmetics) und ein modernes, deutschsprachiges UI mit AI-Karteikarten- und Quiz-Generierung aus PDFs, Notizen oder freiem Text. Plus eine B2B-Schiene für Lehrer mit Klassenzimmern und Assignments — weil das eigentliche Geld in der Schule liegt, nicht bei Hobby-Lernern.
Der Rahmen, in den es passen musste.
throw sagt.Wie es läuft.
◦ Schematische Darstellung, Zahlen illustrativ.
Fünf bewusste Entscheidungen.
Ein zentraler withApiHandler statt Per-Route-Boilerplate.
withApiHandler<T>(handler, opts, schema) macht in einer Funktion: Auth, Zod-Validation, CSRF-Verify, Rate-Limit, Body-Size-Cap, Ban-Check, Audit-Log, Sentry-Capture, Request-ID-Propagation.Credits vor dem AI-Call, nicht danach.
checkCredits() → deductCredits() in eigener Prisma-Transaktion mit Row-Lock → AI-Call → refundUsage() bei FehlerFOR UPDATE) serialisiert parallele Abbuchungen, abgebucht wird, bevor der Call rausgeht, und scheitert der Call, gehen die Credits per refundUsage() in die Töpfe zurück, aus denen sie kamen. Kein AI-Call läuft ohne bereits abgebuchtes Guthaben.FSRS statt SM-2 oder eigenes „smarter“ Schema.
Flashcard.fsrs*-FeldernLokales Redis vor Upstash, mit Memory-Fallback.
redis:7-Container · @upstash/redis als Failover · Map als letzter Fallback · custom Sliding-Window-Rate-LimitCoolify auf einer Box statt Vercel + Managed-Stack.
Coolify auf einem dedizierten Hetzner-Server orchestriert App + Postgres + Redis + Soketi als Docker-Services. Push to main → Webhook → Coolify rebuildet, Traefik switcht den Upstream. DB-Migrationen laufen manuell via docker exec.Was nicht offensichtlich war.
CLOZE-Fallback im Front-End-Validator
{{c1::hidden::hint}} in der Front-Seite gespeichert. Ein AI-Generator hat irgendwann angefangen, den Marker still wegzulassen — der CardType blieb CLOZE, aber die Front-Seite war pure Prosa. Der Lerner sah nichts zu fixen.Lösung: Beim Speichern Downgrade auf STANDARD, wenn keine Marker erkannt werden. Kein Throw, kein Fail-Loud — nur eine stille Korrektur, die die Karte trotzdem nutzbar macht. AI-Outputs sind nie schema-treu; das Backend muss aufräumen, was der Generator falsch labelt.
NotificationPreference als Gate, nicht als Flag
{ pushEnabled: false } auf User. Realität: User wollen Achievement-Notifications per Push, aber Group-Messages nur In-App, und Streak-Warnings per Mail um 18 Uhr.Refactor:
NotificationPreference mit (userId, type, channel) als Composite-Key. createNotification() ruft jeweils isNotificationAllowed() pro Kanal — und returnt null, wenn der User abbestellt hat. Keine Exception, keine Sentry-Noise: ein opted-out-User ist kein Fehler, nur ein No-op. 29 Notification-Types × 3 Channels = 87 mögliche User-Präferenzen, aber alles default-on und per Opt-out abwählbar.Stripe-Webhook idempotent über Event-ID
checkout.session.completed nochmal — und das erste Mal hat den Plan upgedatet, das zweite Mal würde es einen doppelten CreditAdd auslösen.Lösung:
CreditTransaction.stripeEventId als @unique Index. Der Webhook-Handler liest das Event zuerst, erstellt einen signed-and-deduped CreditTransaction-Insert mit der Event-ID — bei Duplicate-Key-Error → already processed, return 200 ohne Side-Effect. Die ganze Subscription-Logik läuft im selben prisma.$transaction. Wenn die Transaktion zurückgerollt wird, wird auch der Insert zurückgerollt — Stripe sendet erneut, dann hat der Insert wieder eine echte Chance. Eine einzige Unique-Constraint, der Rest ist Datenbank-Mechanik.CSP-Nonce aus proxy.ts
middleware.ts deprecated — proxy.ts ist der vorgesehene Hook für Per-Request-Logik. Die CSP nutzt nonce-basiertes script-src (kein unsafe-inline mehr), und der Nonce wird pro Request generiert.proxy.ts setzt x-nonce-Header, layout.tsx liest ihn via headers() und propagiert ihn an alle Inline-Script-Tags. Erlaubt strikte CSP, ohne jeden Inline-Script-Hash zu pflegen — aber jeder neue <Script> muss nonce mitnehmen, sonst CSP-Block. Ein Lighthouse-Drop nach einem fehlenden Nonce-Prop hat das mal live in 30 Sekunden offenbart.Audit-Log als Feature, nicht als Compliance
AuditLog-Row mit Actor + Target + Diff.Nebeneffekt: jeder Support-Fall („meine Karten sind weg“) wird in Sekunden nachvollziehbar — Cron-Job? User selbst? Admin? Diff zeigt, was vorher dran stand. Im Admin-Dashboard ist das ein Filter über Actor-ID, kein Grep über Logs. Compliance fällt als Side-Effect ab, nicht andersrum.
Manuelle SQL-Migrationen statt prisma migrate dev
0_init-Migration — irgendein Whitespace im generierten SQL kippt das Diff. Statt zu fixen: Migrationen werden manuell als migration.sql in prisma/migrations/ abgelegt und mit prisma migrate deploy gefahren.Jede Enum-Erweiterung nutzt
IF NOT EXISTS, damit Re-Runs idempotent sind. Plus: in Prod muss npx prisma@6 gepinnt werden, weil Prisma 7 inkompatible Config-Breaking-Changes hat — und npx ohne Pin pullt die neueste Version. Eine 15-Zeilen-Note im CLAUDE.md, weil mich der Stack zweimal beim Migrieren erwischt hat.Was läuft.
Was ich gelernt habe.
Ein zentraler API-Handler ist die Investition.
withApiHandler ist die einzige Architekturentscheidung, die ich rückwirkend früher getroffen hätte. CSRF, Rate-Limit, Audit-Log, Sentry, Body-Size, Ban-Check, Request-ID — alles Dinge, die per-route nachzurüsten ein Wochenende kostet, aber zentral eine Stunde. Ich habe vier Sicherheits-Cross-Cuts nach dem ersten Live-Tag eingezogen, ohne 200 Routes anzufassen. Bei Solo-Bau ist die Frage „tut das Boilerplate genug weh, dass ich eine Helper-Funktion baue“ der falsche Reflex — die Frage ist „kann ich es mir leisten, einmal zu vergessen“. Bei Auth/CSRF/Audit ist die Antwort immer nein.
Gamification macht Lernen zur Gewohnheit.
Ich war anfangs allergisch gegen Streaks und XP — wirkt billig, manipulativ, „dunkles Pattern“. Heute halte ich sie für den Kern des Produkts — nicht weil User XP wollen, sondern weil das Streak-Badge eine extern sichtbare Selbstverpflichtung ist — dasselbe Prinzip, das Duolingo trägt. Streak-Freezes als „Forgiveness-Mechanik“ verhindern, dass eine vergessene U-Bahn-Fahrt die ganze Habit-Schleife killt. Lernen ist Gewohnheits-Engineering, nicht Algorithmus-Engineering. FSRS ohne Habit-Layer ist brillante Software, die niemand öffnet.