CASE · 012025 — LIVESOLO · END-TO-ENDFULL SAAS · FSRS · AI · STRIPE
◦ FLASHBUDDY · flashbuddy.app

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.

Flashbuddy — Screenshot
live · flashbuddy.app
§ 01 — Problem · motivation

Warum es das gibt.

Anki ist mächtig, aber roh. Quizlet ist hübsch, aber dumm. Beide ignorieren, dass Lernen ein Gewohnheits-Problem ist, kein UI-Problem.

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.

§ 02 — Constraints · operating box

Der Rahmen, in den es passen musste.

Solo-Builder, öffentlich erreichbar, DSGVO-pflichtig. Kein Vercel-Lock-in, keine VC-Burn-Rate, kein „nur wenn skaliert“-Gedanke.
C/01 · OPS
Solo-Dev. Jede Architekturentscheidung muss ohne Team wartbar sein — keine Microservices, keine Event-Bus-Choreografien, kein verteiltes Tracing-Bühnenbild.
C/02 · HOSTING
Coolify auf einem dedizierten Hetzner-Server — App, Postgres, Redis, Soketi laufen alle als Docker-Services unter einer Orchestrierung. Auto-Deploy bei push-to-main via Webhook, Cloudflare als Edge davor (SSL, Zero-Trust). Keine Cloud-Lock-Kosten, der ganze Stack kostet weniger als ein einziges AWS-RDS.
C/03 · DSGVO
Made in Germany, EU-Hosting, kompletter DSGVO-Datenexport über alle 28+ Prisma-Relationen, Account-Löschung mit harter Kaskade. PostHog auf EU-Endpoint.
C/04 · CREDITS
AI ist nicht gratis. Vier Tiers (FREE/STARTER/PREMIUM/PRO) mit monatlichem Credit-Reset und Top-up-Käufen. Pro Aktion 1–5 Credits, sauber abgerechnet — bevor der Call rausgeht.
C/05 · OFFLINE
PWA mit IndexedDB + Background-Sync. Pendler lernen in der U-Bahn — wer Reviews verliert, weil das WLAN flackert, kommt nicht zurück.
C/06 · SECURITY
User-generated Content, AI-generated Content, Stripe-Webhooks, API-Keys. CSRF (Header-Verify, keine Token), nonce-basierte CSP, Audit-Log mit DB-Trail, Sentry für alles, was throw sagt.
C/07 · REAL-TIME
29 Notification-Types — AI fertig, Freund-Anfrage, Challenge gewonnen, Achievement freigeschaltet. WebSocket via Soketi, Polling als Fallback, NotificationPreference pro User pro Kanal.
C/08 · I18N FLOOR
Komplett deutsch — keine englische Standard-Stringhalde, jede Fehlermeldung als ERR-Konstante in error-messages.ts. Eine englische Übersetzungsschicht, die weiterläuft, ohne dass Strings im Code stehen.
§ 03 — Architecture · request to review

Wie es läuft.

Drei Spuren: oben der synchrone Request-Pfad (Browser → withApiHandler-Pipeline → Postgres), Mitte die AI-Spur (Credits-Gate → OpenAI → Generator → DB), unten die Side-Effects (Soketi-WebSocket, Stripe-Webhook, Sentry). Redis für Rate-Limit + Cache, alles auf einer einzigen Hetzner-Box, davor Cloudflare-Edge.
flashbuddy.app·host coolify·deploy push-to-main
tiers FREE / STARTER / PREMIUM / PRO
CLIENT · 01
PWA · IndexedDB
Next.js 16 · service worker
offlinebg-sync
HANDLER · 02
withApiHandler
CSRF · Zod · rate · audit
latency142 ms
DATA · 03
Prisma 6 · Postgres 16
28+ relations · txn boundary
migrationsmanual SQL
EDGE · 04
Cloudflare · Traefik
SSL · Zero Trust · nonce CSP
orchestratorcoolify
GATE · 05
Credits · checkCredits()
pre-call · row lock
cost1–5 cr
AI · 06
OpenAI router
nano · mini · vision
gpt-5-nanogpt-5-minivision
GEN · 07
Card · Quiz · PDF · OCR
cloze fallback · schema fix
FSRS · 08
Spaced repetition
per-user retention
modelmemory-stable
CACHE · 09
Redis 7 · ioredis
sliding-window · upstash
limits100/20/5 rpm
PUSH · 10
Soketi · WebSocket
29 types · polling fallback
channelsprivate + presence
BILLING · 11
Stripe · 4 tiers + packs
idempotent webhooks · txn-safe
eventsdeduped by id
OBS · 12
Sentry · audit log
request-id · diff trail
tracex-request-id
EVENT LOG · /api/audit · request-id · diff trail
14:22:09openaigen.cards · 24 cloze · pdf-import · 2 cr · 1.84s
14:22:08stripewebhook · invoice.paid · STARTER → PREMIUM · deduped
14:22:07handlerPOST /api/decks · zod ok · csrf ok · 142ms
14:22:06soketiquest.completed · presence · 3 listeners · 8ms
14:22:04openaigen.quiz · 8 mc · history-101 · 2 cr · 0.91s
14:22:02handlerGET /api/decks/42/cards · 200 · 38ms
14:22:01redisrate.ok · user:1247 · 12/100 · sliding 60s
14:21:59fsrsschedule · 14 due · interval 3.2d → 6.1d
14:22:09openaigen.cards · 24 cloze · pdf-import · 2 cr · 1.84s
14:22:08stripewebhook · invoice.paid · STARTER → PREMIUM · deduped
14:22:07handlerPOST /api/decks · zod ok · csrf ok · 142ms
14:22:06soketiquest.completed · presence · 3 listeners · 8ms
14:22:04openaigen.quiz · 8 mc · history-101 · 2 cr · 0.91s
14:22:02handlerGET /api/decks/42/cards · 200 · 38ms
14:22:01redisrate.ok · user:1247 · 12/100 · sliding 60s
14:21:59fsrsschedule · 14 due · interval 3.2d → 6.1d

◦ Schematische Darstellung, Zahlen illustrativ.

§ 04 — Decisions · trade-offs

Fünf bewusste Entscheidungen.

Pro Entscheidung: was gewählt, statt was, warum.
D/01

Ein zentraler withApiHandler statt Per-Route-Boilerplate.

chosen
Generischer 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.
instead of
Pro Route 30 Zeilen Boilerplate für dieselbe Sicherheitskette
reason
Bei 200+ API-Routes ist Copy-Paste-Sicherheit eine Frage des Wann, nicht des Ob. Eine vergessene CSRF-Validation oder ein fehlendes Rate-Limit pro Route ist genau die Klasse von Bugs, die aus einem Audit-Log rückwirkend nicht mehr fixbar ist. Ein zentraler Handler erzwingt die Defaults: Auth ist opt-out, nicht opt-in. Tests mocken eine einzige Surface, Error-Handling ist konsistent, neue Cross-Cutting-Concerns (Audit-Log kam später dazu) landen an einer einzigen Stelle. Die Abstraktion zahlt sich ab Route 4 aus, danach ist sie reine Schwerkraft.
D/02

Credits vor dem AI-Call, nicht danach.

chosen
checkCredits() → deductCredits() in eigener Prisma-Transaktion mit Row-Lock → AI-Call → refundUsage() bei Fehler
instead of
Optimistisches Abziehen post-hoc oder Soft-Limits aus Logs
reason
OpenAI kostet echtes Geld pro Request. Ein User auf FREE (20 Credits/Monat) darf bei einer leeren Wallet keinen gpt-5-mini-Vision-Call mehr triggern — auch dann nicht, wenn zwei Tabs parallel klicken. Der Row-Lock (FOR 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.
D/03

FSRS statt SM-2 oder eigenes „smarter“ Schema.

chosen
FSRS-6-Implementierung mit den 21 publizierten Default-Weights, State pro Karte (State · Stability · Difficulty · LastReview · NextReview) auf den Flashcard.fsrs*-Feldern
instead of
SM-2 nachbauen oder ein Eigen-Schema „mit Vibes“
reason
SM-2 ist 1990er-Stand und unterschätzt schwere Karten systematisch. Ein eigener Algorithmus ist der schnellste Weg, ein Lern-Tool unbeweisbar zu machen — niemand kann später sagen, ob du Anki schlägst, weil du keinen peer-reviewed-Vergleichspunkt hast. FSRS-6 ist memory-stability-modelliert, hat eine wachsende Forschungs-Community, Anki selbst migriert dorthin. Die Default-Weights laufen ab dem ersten Review — Re-Training pro User ist eine Aufrüst-Option, die ich später dranflanschen kann, ohne den Persistenz-Layer zu ändern. Rückwirkend keine schlechtere Entscheidung getroffen.
D/04

Lokales Redis vor Upstash, mit Memory-Fallback.

chosen
ioredis gegen lokalen redis:7-Container · @upstash/redis als Failover · Map als letzter Fallback · custom Sliding-Window-Rate-Limit
instead of
@upstash/ratelimit + Upstash REST als einzige Wahrheit
reason
Upstash REST ist pro-Call gepricet und latenzanfällig — bei 100 req/min/User wird das schnell teuer, und das Rate-Limit selbst sitzt zwischen Browser und Origin auf dem Hot-Path. Lokaler Redis-Container auf derselben Box: ~0,3 ms Roundtrip, frei, deterministisch. Upstash REST bleibt als gleiche Schnittstelle, falls Redis stirbt — und ein In-Memory-Map-Fallback hält Tests und Dev-Maschinen am Leben, die kein Redis brauchen. Der Rate-Limit-Code ist custom (sliding window über sortierte Sets) statt @upstash/ratelimit, weil ich den Algorithmus debugbar haben wollte und drei Backends statt eines bedienen muss.
D/05

Coolify auf einer Box statt Vercel + Managed-Stack.

chosen
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.
instead of
Vercel + Managed-Postgres (Neon) + Managed-Redis (Upstash) — oder k8s
reason
Vercel+Neon+Upstash ist der Default-„Go-fast“-Stack — und summiert sich für ein SaaS mit AI-Calls, ständigem Postgres-Traffic und Soketi-WebSockets schnell auf dreistellig pro Monat, lange bevor User dafür zahlen. Ein Hetzner-Dedicated trägt den ganzen Stack inklusive Datenbank für einen Bruchteil dessen, und Coolify gibt mir die Vercel-DX (push-to-deploy, Build-Logs, ENV-UI) ohne Lock-in. Migration auf einen anderen Anbieter ist compose-up‑woanders. Was an Magie fehlt, fehlt absichtlich: ein Solo-Dev, der k8s betreibt, ist ein Solo-Dev, der nichts mehr ausliefert.
§ 05 — Highlights · interesting bits

Was nicht offensichtlich war.

Edge-Cases und Details, die erst beim Bauen klar wurden.

CLOZE-Fallback im Front-End-Validator

H/01
Cloze-Karten werden mit Markern {{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

H/02
Erste Version: User togglt Notifications, App speichert { 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

H/03
Stripe sendet jeden Webhook bis zur 2xx-Antwort. Bei Timeout oder 500 kommt derselbe 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

H/04
In Next.js 16 ist 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

H/05
DSGVO erzwingt eine User-Daten-Löschung mit Beleg. Ich habe das Audit-Log nicht für DSGVO gebaut, sondern um Bug-Reports debuggen zu können — jede sicherheitsrelevante Aktion (Login, Plan-Wechsel, Card-Delete, Admin-Override) landet als 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

H/06
Prismas Shadow-Database scheitert reproduzierbar an der 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.
§ 06 — Stack · in production

Was läuft.

Eingesetzte Toolchain — nichts Theoretisches.
Next.js 16 · App RouterTypeScript 5Prisma 6 · Postgres + pgvectorNextAuth.js v5 · JWTTailwind 4 · shadcn/uiRedis · ioredis · sliding-windowStripe · Subs + Credit-PacksOpenAI · gpt-5-nano · gpt-5-miniFSRS · per-user retentionSoketi · polling fallbackSentry · proxy + audit-logPWA · IndexedDB · BG syncTipTap · rich notesCoolify · dedicated HetznerCloudflare · Traefik · push-to-deployVitest · Playwright · k6
§ 07 — Reflection · takeaways

Was ich gelernt habe.

Live als SaaS auf flashbuddy.app. Diese Dinge nehme ich in die nächsten mit.

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.

◦ NEXT CASE · 02 / 10
Capypad → ↗
← alle Projekte