[ case study ]

AI-Native Learning Platform & The Over-Engineering Trap

Zbudowałem techniczne monstrum oparte o Modular Monolith, polimorficzny silnik notatek i orkiestrację AI, które wyglądało jak gotowy produkt po rundzie seed. Poznaj historię ambitnego projektu, który miał zrewolucjonizować naukę, a zamiast tego stał się moją najważniejszą lekcją budowania MVP.

[ archived ]

Full-Stack Developer & Product Owner

Stack

Główny workspace: grupa, moduły (Notes, Quiz, Fiszki) i edytor z blokami matematycznymi.

Główny workspace: grupa, moduły (Notes, Quiz, Fiszki) i edytor z blokami matematycznymi.

Goal

Eliminacja tarcia w procesie nauki. Przejście od statycznego „magazynu” notatek do środowiska gdzie uczniowie razem mogą współpracować w grupach nad notatkami a AI pomaga pisać notatki, natywnie przekształca wiedzę w interaktywne quizy, fiszki i sesje z wirtualnym tutorem.

Przewiń dalej

01 - Wprowadzenie

GroupNote - AI-native collaborative learning

Większość case studies, które czytasz, to historie projektów z happy endem. Ten jest inny.

Groupnote to platforma edukacyjna, która miała połączyć wspólne tworzenie notatek z natywnym wsparciem AI. Z technicznego punktu widzenia to najbardziej zaawansowany system, jaki zaprojektowałem: zbudowałem silnik notatek oparty na polimorficznym jsonb, zintegrowałem w czasie rzeczywistym system synchronizacji (bypassing backendu) i stworzyłem własną orkiestrację LLM (Gemini) opartą na mikroserwisie w Pythonie komunikującym się z monolitem w .NET oraz frontend w React.

Aplikacja wyglądała jak gotowy SaaS po rundzie seed. Posiadała wszystko: od edytora notatek współdzielonych, przez AI Tutora, Quizy, Fiszki aż po gotowy system limitów i subskrypcji.

Problem? Nigdy jej nie wypuściłem.

To case study o tym, jak połączyć fascynującą, nowoczesną architekturę z klasycznym błędem "Founder Trap" - potężnym scope creepem i budowaniem ogromnego systemu zamiast zwalidowania małego MVP. To projekt, który nauczył mnie myśleć nie tylko jak programista, ale jak inżynier produktu.

Status: Zarchiwizowane (nigdy nie opublikowano publicznie).

Rola: Full-Stack Developer & Product Owner

Cel: Stworzenie środowiska nauki AI Native, w którym AI natywnie przekształca notatki w quizy, fiszki i interaktywne sesje z wirtualnym tutorem.

Widok produktu - szkielet nawigacji (Dashboard, Notes, Quizzes, Flashcards) oraz wejście do trybu nauki z Lernie.
Moduł Notes: strona kursu, zakładki trybów nauki, sidebar hierarchii stron oraz edytor blokowy z importem, asystentem AI i typami bloków (Text, Heading, Code, Math).

Tech Stack

Backend & Architecture (Modular Monolith)

  • C# / .NET 9 (Minimal APIs, CQRS z MediatR, FluentValidation)

  • PostgreSQL (nacisk na architekturę jsonb do zapisu struktury blokowej edytora)

  • Redis (caching)

Frontend & UX

  • React 18 & Vite 5 (TypeScript)

  • TanStack Query (data fetching & state management)

  • Radix UI & Tailwind CSS (design system)

  • Framer Motion (mikroanimacje)

Real-time & AI Layer

  • Supabase (Auth & Realtime postgres_changes - synchronizacja notatek na żywo)

  • Python (mikroserwis) (kontener pod komunikację z Gemini API, separacja logiki AI od domeny w .NET)

  • Docker & Aspire AppHost (lokalna orkiestracja środowiska)

02 - Problem & Idea

The Idea: Od magazynu wiedzy do aktywnego środowiska nauki

Większość popularnych narzędzi do robienia notatek (Notion, Microsoft OneNote, Evernote) została zaprojektowana z myślą o jednym głównym celu: przechowywaniu wiedzy. Działają jak nieskończone, cyfrowe segregatory.

Jednak dla uczniów i studentów samo "zapisanie" informacji to dopiero początek. Prawdziwa nauka wymaga aktywnego przetwarzania. Zauważyliśmy, że standardowy proces nauki to w 80% żmudne przygotowania:

  • przepisywanie notatek z tablicy lub zeszytów do komputera,

  • ręczne wyciąganie definicji i tworzenie z nich fiszek (np. w Anki),

  • samodzielne układanie pytań, żeby przetestować swoją wiedzę przed sprawdzianem.

Problem: Narzędzia, z których korzystaliśmy, zmuszały nas do skakania między 4 różnymi aplikacjami, aby po prostu zacząć się uczyć.

Główna idea stojąca za Groupnote opierała się na jednym prostym pytaniu:

Co by było, gdyby sztuczna inteligencja mogła zamienić Twoje statyczne notatki w kompletne, interaktywne środowisko nauki?

Zamiast traktować AI jako doczepionego z boku chatbota, postanowiliśmy uczynić z niego natywny silnik napędzający całą aplikację. Chcieliśmy całkowicie odwrócić proporcje - system miał wziąć na siebie 80% pracy przygotowawczej, pozwalając uczniom skupić się wyłącznie na przyswajaniu wiedzy.

Zaprojektowaliśmy przepływ pracy (workflow), który likwidował wszelkie tarcia w procesie nauki:

  1. Cyfryzacja bez wysiłku (OCR to Blocks): Robisz zdjęcie odręcznej notatki z zeszytu. System (dzięki integracji AI i bucketów S3) nie tylko odczytuje tekst, ale mapuje go na naszą strukturę polimorficznych bloków - automatycznie rozpoznając nagłówki, listy, a nawet wzory matematyczne.

    Import Notes from Files: upload obrazu lub PDF, OCR i mapowanie do natywnej struktury blokowej edytora (Images vs PDF).
  2. Note completion: Podczas pisania notatki AI podpowiadało następną część - taki GitHub Copilot dla notatek. Użytkownik mógł nacisnąć Tab, żeby zaakceptować sugestię, lub pisać dalej, aby ją odrzucić.

    Smart Completions: podpowiedź inline w edytorze, akceptacja Tabem - ten sam wzorzec co w IDE, ale z kontekstem notatki.
  3. Kontekstowa pomoc: Nie rozumiesz fragmentu tekstu? Zaznaczasz go w edytorze i używasz opcji Explain lub Correct, a system tłumaczy to na miejscu, bez opuszczania dokumentu.

    Zaznaczenie w bloku matematycznym: menu Lernie (Explain / Correct) bez zmiany kontekstu dokumentu.
  4. Aktywna nauka z jednego kliknięcia: Kończysz temat i klikasz jeden przycisk. System analizuje kontekst notatki i generuje z niej talię fiszek lub pełnoprawny, interaktywny quiz czy zestaw pytań otwartych.

  5. Wiedza współdzielona (Multiplayer): Wszystko dzieje się w czasie rzeczywistym, w przestrzeni grupowej, gdzie możesz współpracować z kolegami z klasy nad tym samym dokumentem.

    Shell produktu: kontekst grupy, zakładki modułów (Notes, Quizzes, Lernie) i współdzielony dokument - realtime pod spodem.

Groupnote nie miało być miejscem, gdzie "trzymasz" notatki. Miało być miejscem, gdzie "uczysz się" z notatek.

Moduł importu: konwersja zdjęcia odręcznej notatki bezpośrednio do natywnej struktury blokowej edytora.

Zaznaczasz tekst i od razu masz dostęp do AI (Explain, Correct, Flashcards) - zero przełączania kontekstu.

03 - Produkt & Funkcje

Product Features: Silnik nauki

Rdzeń: blokowy edytor (Notion-style)

Sercem Groupnote był w pełni autorski, blokowy edytor tekstowy (inspirowany Notion). Nie chcieliśmy polegać na prostym polu textarea czy zapisywaniu wszystkiego jako surowy HTML.

Każdy element notatki był niezależnym obiektem (blokiem) przechowywanym w bazie PostgreSQL jako polimorficzny jsonb. To pozwoliło nam na obsługę zaawansowanych struktur, takich jak:

  • Rich Text (z formatowaniem inline),

  • Code Blocks (z wyborem języka i syntax highlightingiem),

  • Math Blocks (natywne wsparcie dla wzorów matematycznych),

  • Image Blocks (upload obrazów zintegrowany z bucketami S3),

  • Listy, nagłówki i tabele (przygotowane po stronie backendu).

Dzięki architekturze blokowej backend rozumiał strukturę dokumentu, co było kluczowe dla późniejszego karmienia tymi danymi modeli AI.

Screen: edytor z widocznymi typami bloków (kod, matematyka, tekst).

Edytor wspierający bloki tekstowe, kod, wzory matematyczne i obrazy.

Widok produktu: grupa studyjna, zakładki Notes / Quizzes / Flashcards, sidebar stron oraz edytor z blokami matematycznymi i Lernie w nagłówku.

W samym edytorze działał też AI Writing Assistant z podpowiedziami inline (Smart Completions) - akceptacja klawiszem Tab, bez wychodzenia z kontekstu notatki.

Smart Completions: podpowiedzi inline w trakcie pisania (Tab do akceptacji) - ten sam UX co w IDE, osadzony w dokumencie notatki.

Cyfryzacja bez tarcia (AI OCR)

Jednym z największych problemów w nauce jest tarcie na samym początku - konieczność ręcznego przepisywania notatek z zeszytu do komputera. Zbudowaliśmy mechanizm, który to całkowicie eliminował.

Użytkownik mógł zrobić zdjęcie odręcznej notatki. System przesyłał je do S3, a następnie procesował przez modele AI. Wynikiem nie był jednak zwykły, płaski tekst. System analizował strukturę zdjęcia i automatycznie mapował ją na nasze natywne bloki: rozpoznawał nagłówki, tworzył listy, a odręczne równania zamieniał na bloki matematyczne.

Screen: modal importu / ścieżka OCR do bloków.

Odręczna notatka przekonwertowana natywnie na system blokowy w edytorze.

Import Notes from Files: upload obrazu lub PDF, OCR i mapowanie do drzewa bloków (zakładki Images / PDF).

Po zamianie surowych materiałów na bloki, kolejny krok był naturalny: dać uczniowi pomoc AI dokładnie w miejscu, w którym czyta i pisze.

Kontekstowe AI (Explain & Correct)

Zamiast zmuszać ucznia do otwierania nowej karty z ChatGPT za każdym razem, gdy czegoś nie rozumiał, zintegrowaliśmy AI bezpośrednio z interfejsem edytora.

Zaznaczenie dowolnego tekstu wywoływało popover z akcjami AI. Opcja Explain tłumaczyła zawiłe koncepcje, a opcja Correct poprawiała błędy w notatkach. Wszystko działo się w kontekście czytanego właśnie fragmentu, utrzymując użytkownika w stanie flow.

Screen: zaznaczenie i menu Lernie (Explain / Correct).

Zaznaczasz tekst, a AI tłumaczy trudne pojęcia bez opuszczania kontekstu notatki.

Fabryka nauki (quizy, fiszki, pytania otwarte)

To tutaj Groupnote zamieniało się w prawdziwą platformę EdTech. Notatka była tylko punktem wyjścia. Z poziomu jednego przycisku system potrafił przeanalizować zawartość dokumentu i wygenerować narzędzia do aktywnej nauki:

  1. AI Flashcards: automatyczne generowanie talii fiszek z najważniejszymi definicjami.

  2. AI Quizzes: testy wielokrotnego wyboru do szybkiej weryfikacji wiedzy.

  3. Open-Ended Question Sets: najbardziej zaawansowany edukacyjnie moduł - AI generowało pytania otwarte, potem jak nauczyciel: ocena, punktacja, feedback i wzorcowa odpowiedź.

Błyskawiczna konwersja statycznych notatek w interaktywne materiały do nauki.

"Lernie" - osobisty tutor AI

Jeśli quizy i fiszki nie wystarczały, uczeń mógł otworzyć panel boczny i porozmawiać z Lernie - dedykowanym wirtualnym tutorem. Lernie miał stały dostęp do kontekstu całej notatki, więc nie trzeba było wklejać tekstu. Zaimplementowaliśmy też tony wypowiedzi (Casual, Academic, Professional) - od wykładu po ton "starszego kolegi z ławki".

Screen: czat Lernie w sidebarze (do uzupełnienia w kolejnej iteracji materiałów).

Lernie ma pełny kontekst notatek i potrafi tłumaczyć materiał w wybranym tonie (Casual / Academic / Professional).

Popover na zaznaczeniu: Explain i Correct w miejscu, bez zmiany karty przeglądarki.

Realtime multiplayer

Nauka rzadko odbywa się w próżni. Cały system działał w środowisku zsynchronizowanym w czasie rzeczywistym. Dzięki Supabase Realtime wiele osób w grupie mogło jednocześnie edytować tę samą notatkę, widząc kursory i zmiany natychmiast, bez odświeżania strony.

Nauka w grupie

Uczniowie zapraszali się do grup. Grupy miały wspólne notatki, quizy, fiszki i zestawy pytań otwartych. Koszt subskrypcji mógł rozłożyć się na kilka osób - model znany z Spotify czy Netflix.

04 - Architektura Systemu

Technical Architecture: Engine room produktu

Teza architektoniczna: pragmatyzm ponad hype

Przy systemie łączącym współedycję, operacje AI i wieloosobową naukę łatwo wpaść w hype-driven development i rozbić całość na kilkanaście mikroserwisów.

Wybrałem Modular Monolith dla głównego API, osobny mikroserwis AI oraz delegację realtime bezpośrednio do warstwy danych. Ten układ dawał szybkie iteracje i czytelne granice kodu, bez blokowania przyszłego skalowania.

Architektura high-level: React 18 -> ASP.NET Core 9 API -> moduły domenowe, PostgreSQL/Supabase + Redis, oraz orkiestracja AI przez dedykowany serwis Python -> Gemini.

Schemat pokazuje warstwy, które są niezależne odpowiedzialnością, ale ściśle współpracują kontraktami HTTP i modelem danych.

Stack warstwy systemowej

Backend (.NET 9) i vertical slices z CQRS

Główne API Groupnote to ASP.NET Core 9. Zamiast klasycznej architektury warstwowej, monolit został podzielony na moduły i pionowe slice'y: Users, Groups, Notes, Quizzes, Tutor, AI, RecentActivity.

  • CQRS + MediatR: jawne Command i Query per use-case.

  • Validation + Rate Limit Pipeline: reguły planów i walidacje przed handlerem.

  • DDD boundaries: każda domena ma własne encje, błędy i kontrakty.

csharp
public sealed record CreateQuizCommand(Guid NoteId, Guid UserId)
  : IRequest<CreateQuizResponse>;

public sealed class CreateQuizCommandHandler
  : IRequestHandler<CreateQuizCommand, CreateQuizResponse>
{
  public async Task<CreateQuizResponse> Handle(
    CreateQuizCommand command,
    CancellationToken ct)
  {
    // validation + plan limits run in MediatR pipeline
    var note = await notesRepository.GetAsync(command.NoteId, ct);
    var quiz = await aiOrchestrator.GenerateQuizAsync(note, ct);
    return await quizzesRepository.SaveAsync(quiz, command.UserId, ct);
  }
}

Pattern orkiestracji AI (.NET -> Python)

Kluczowa decyzja: w kodzie .NET nie trzymamy SDK modeli LLM. Moduł AI działa jako orkiestrator i używa IHttpClientFactory do komunikacji z mikroserwisem Python.

Python obsługuje modele Gemini, prompting, OCR i normalizację odpowiedzi do JSON. .NET pozostaje systemem rekordowym - autoryzacja, billing, limity i zapis danych.

yaml
orchestration:
  entrypoint: dotnet_ai_module
  transport: http_internal
  python_service:
    endpoint: http://python-ai:4000
    models:
      ocr: gemini-1.5-pro
      tutor_chat: gemini-1.5-flash
  response_contract:
    format: json
    persisted_by: aspnet_api

Infrastructure i local development

Całość środowiska lokalnego była spinana przez .NET Aspire (AppHost) oraz Docker, co pozwalało uruchomić API, PostgreSQL, Redis i AI service jednym poleceniem.

Dzięki temu każdy deweloper w zespole dostawał taki sam runtime i przewidywalny deployment path od local do środowisk testowych.

05 - Silnik Notatek

Notes Engine: Polimorficzny system bloków na PostgreSQL jsonb

To jest sekcja z najtwardszym mięsem technicznym całego case study. Fundamentem Groupnote był silnik notatek oparty o niezależne bloki, a nie jeden długi HTML string.

Model dokumentu: NoteBlock jako baza i typy potomne (Text, Code, Math, List, Image) mapowane do tej sąmej struktury danych.

Dlaczego HTML/Markdown jako TEXT to ślepa uliczka

Klasyczny model WYSIWYG + zapis HTML w kolumnie TEXT działa dla prostego bloga, ale nie dla produktu z realtime i AI.

  • Parsowanie zagnieżdżonego HTML przez modele AI marnuje tokeny i podnosi ryzyko błędów.

  • Semantyka dokumentu (nagłówki, kod, matematyka, obrazki) ginie w jednym stringu.

  • Synchronizacja pojedynczych zmian między użytkownikami jest dużo bardziej konfliktogenna.

Dlatego wybraliśmy model block-based inspirowany Notion - każdy fragment notatki ma własne ID i typ.

Model domenowy w C#

Backend korzysta z polimorficznej hierarchii klas. Nowe typy bloków można dodawać bez przebudowy całej domeny.

NoteBlock.cs
// Base class for every editor element
public abstract class NoteBlock
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    [JsonPropertyName("type")]
    public string Type { get; set; } = string.Empty;

    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
}

public sealed class TextBlock : NoteBlock
{
    public TextBlock() => Type = "text";
    public string Content { get; set; } = string.Empty;
}

public sealed class CodeBlock : NoteBlock
{
    public CodeBlock() => Type = "code";
    public string Language { get; set; } = "plaintext";
    public string Code { get; set; } = string.Empty;
}
Note.cs
public sealed class Note
{
    public Guid Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public List<NoteBlock> Content { get; set; } = new();
    public Guid UserId { get; set; }
    public Guid GroupId { get; set; }
}
Implementacja w UI: struktura notatki oparta o bloki (Text/Heading/Code/Math) i edycja granularna zamiast operacji na całym dokumencie.

Persistencja: PostgreSQL jsonb + EF Core

Relacyjne tabele per typ bloku byłyby over-engineeringiem. Zamiast tego użyliśmy jsonb i mapowania przez EF Core + Npgsql.

NoteConfiguration.cs
// NoteConfiguration.cs
builder.Property(x => x.Content)
    .HasColumnType("jsonb")
    .IsRequired()
    .HasConversion(
        v => JsonSerializer.Serialize(v, serializerOptions),
        v => JsonSerializer.Deserialize<List<NoteBlock>>(v, serializerOptions)
             ?? new List<NoteBlock>()
    );

Polimorficzna deserializacja

Kluczowe było poprawne mapowanie `type` na klasę C#. Do tego użyliśmy JsonTypeInfoResolver, żeby blok `code` trafiał do `CodeBlock`, a `text` do `TextBlock`.

note-content.json
[
  {
    "id": "blk-123",
    "type": "heading1",
    "content": "Architektura CPU"
  },
  {
    "id": "blk-456",
    "type": "text",
    "content": "Procesory składają się z rejestrów oraz ALU."
  },
  {
    "id": "blk-789",
    "type": "image",
    "url": "https://storage.supabase.co/...",
    "description": "Schemat blokówy ALU"
  }
]

Dlaczego to była decyzja produktowo-kluczowa

  • AI Context Parsing: backend wysyłal do AI przewidywalny JSON, więc modele mogły selektywnie analizować tylko potrzebne typy bloków.

  • Real-time Synchronization: frontend wysyłal aktualizacje konkretnego bloku, a nie całego dokumentu, co ograniczało konflikty współedycji.

06 - Realtime & Sync

Real-time System: Skalowanie przez ominięcie backendu

Ta sekcja pokazuje krytyczną decyzję architektoniczną: ruch realtime został wyprowadzony poza API .NET, dzięki czemu backend pozostał bezstanowy i skupiony na logice biznesowej.

Pułapka klasycznego podejścia: dlaczego nie SignalR

Pierwszy odruch przy collaborative editorze w .NET to SignalR. W tym MVP byłby to jednak kosztówny skrót: API musiałoby utrzymywać tysiące drobnych eventów i stan połączeń.

To prowadzi do bottlenecku pamięci i droższego skalowania (np. backplane). Dlatego realtime został świadomie usunięty z warstwy API C#.

Architektura direct-to-database

Zamiast budować własny broker eventów, frontend subskrybuje postgres_changes w Supabase Realtime. Dla backendu synchronizacja na żywo praktycznie nie istnieje - API pozostaje bezstanowe.

Flow realtime: klient edytuje lokalnie, debounce wysyła UPDATE do Supabase/PostgreSQL, a postgres_changes uruchamia natychmiastowy broadcast do innych klientów.

Jak to działa w praktyce: frontend + debouncing

Debounce chroni bazę przed zapisem na każdy klawisz i redukuje szum sięciowy. Klient dostaje natychmiastowy feedback lokalnie, a write do bazy dzieje się po krótkiej ciszy.

  1. Uczeń edytuje TextBlock - zmiana trafia od razu do lokalnego stanu React.

  2. Po 1 sekundzie ciszy frontend wysyła pojedynczy UPDATE dla konkretnej notatki.

  3. Supabase rozgłasza zmianę do wszystkich klientów subskrybujących dokument.

subscribeToNoteChanges.ts
const channel = supabase
  .channel(`note_${noteId}`)
  .on(
    "postgres_changes",
    {
      event: "*",
      schema: "public",
      table: "Notes",
      filter: `Id=eq.${noteId}`,
    },
    (payload) => {
      if (payload.new) {
        updateLocalState(payload.new.Content);
      }
    }
  )
  .subscribe();
persistNoteDebounced.ts
const persistNote = debounce(async (noteId: string, content: NoteBlock[]) => {
  await supabase
    .from("Notes")
    .update({ Content: content, UpdatedAt: new Date().toISOString() })
    .eq("Id", noteId);
}, 1000);

const presence = supabase.channel(`presence_note_${noteId}`, {
  config: { presence: { key: userId } },
});
Współedycja w praktyce: użytkownik widzi treść notatki i obecność innych osób (w tym aktywność w obrębie dokumentu) bez odświeżania strony.

Single Source of Truth

Największa zaleta tego układu to spójność danych. Frontend i backend zapisują do tej sąmej tabeli Notes, a Supabase automatycznie wypycha zmiany do klientów.

Klient nie musi wiedzieć, czy update przyszedł od innego ucznia, czy z asynchronicznego pipeline AI (np. OCR).

Efekt końcowy decyzji architektonicznej

  • Mniejsza latencja - znika dodatkowy hop przez API.

  • Niższy koszt infrastruktury - API .NET pozostaje lekkie i biznesowe.

  • Stabilny multiplayer out of the box dzięki delegacji realtime do wyspecjalizowanej warstwy.

07 - Warstwa AI

AI System: Orkiestracja, RAG i AI Copilot

Pipeline AI: .NET jako orkiestrator, Python jako AI engine, Gemini jako warstwa inferencji.

Podział obowiązków: .NET orchestrator, Python AI engine

Groupnote było projektowane jako produkt AI-native, ale bez mieszania SDK modeli LLM bezpośrednio w kontrolerach ASP.NET Core.

  • ASP.NET Core (monolit): system rekordowy, autoryzacja, limity, logika biznesowa, persystencja.

  • Python (mikroserwis): budowanie promptów, wywołania Gemini, normalizacja odpowiedźi do JSON.

Flow orkiestracji requestów (IHttpClientFactory + MediatR)

  1. Frontend wysyła request do API .NET.

  2. Backend sprawdza limity planu w Redis.

  3. Handler pobiera kontekst notatki (jsonb) i deleguje call do serwisu Python przez HTTP.

  4. Python zwraca wynik jako DTO/JSON, a .NET zapisuje rezultat.

GenerateQuizCommandHandler.cs
public async Task<Result<QuizDto>> Handle(
    GenerateQuizCommand command,
    CancellationToken ct)
{
    var limits = await limitsService.GetForUserAsync(command.UserId, ct);
    if (!limits.CanGenerateQuiz) return Result.Fail(AiErrors.LimitExceeded);

    var note = await notesRepository.GetAsync(command.NoteId, ct);
    var request = QuizGenerationRequest.From(note.Content);

    var dto = await aiHttpClient.PostAsJsonAsync<QuizDto>(
        "/api/quiz/generate",
        request,
        ct);

    await quizzesRepository.SaveAsync(dto, command.UserId, ct);
    return Result.Ok(dto);
}
Quiz generation flow: .NET orchestruje limity i dane, Python generuje pytania i odpowiedźi.
Open questions: AI ocenia odpowiedź, zwraca score, feedback i przykładowa odpowiedź.
Flashcards pipeline: ekstrakcja kluczowych faktow i gotowa sesja nauki.

AI Copilot i kontrola kosztów (HybridCache + cooldown)

Note Completion działa podobnie do Copilota, ale jest bardzo wrażliwy na spam i koszty inferencji. Dlatego backend wprowadza twardy cooldown.

NoteCompletionCooldownService.cs
var key = $"ai:completion:cooldown:{userId}";
var lastCall = await hybridCache.GetOrCreateAsync<DateTime?>(
    key,
    _ => ValueTask.FromResult<DateTime?>(null),
    token: ct);

if (lastCall is not null && DateTime.UtcNow - lastCall < TimeSpan.FromSeconds(30))
{
    return Result.Fail(NoteErrors.CooldownActive);
}

await hybridCache.SetAsync(key, DateTime.UtcNow, token: ct);
AI Copilot: sugestia kolejnej części notatki na bazie lokalnego kontekstu dokumentu.

Tutor Memories i RAG personalizacyjny

Lernie dostaje nie tylko treść notatki, ale też preferencje użytkownika (za zgodą) - np. ton wyjaśnień i styl nauki.

TutorMemoryContext.json
{
  "userId": "usr_123",
  "memoryContext": [
    "User prefers game-based examples",
    "User struggles with memorizing dates"
  ],
  "tone": "Academic",
  "noteContext": "..."
}
Core Memories: kontrolowany przez usera kontekst, który podnosi jakość i personalizację odpowiedzi tutora.

Jedna warstwa AI, wiele przypadków użycia

  • Vision/OCR: konwersja zdjęć na bloki notatki.

  • Generative: quizy, fiszki, pytania otwarte.

  • Contextual: Explain/Correct i completion in-płaće.

08 - Monetyzacja

Monetyzacja & Kontrola Kosztów

Strategia wzrostu przez grupy

Sam produkt to za mało - w EdTech trzeba jeszcze znaleźć model, który skaluje się mimo ograniczonych budżetów uczniów.

Dlatego Groupnote od początku było projektowane wokół grup. Zamiast prostego modelu „płać tylko za siebie”, postawiliśmy na wariant podobny do family plans znanych z platform subskrypcyjnych.

Jeden płatnik w grupie odblokowuje wartość dla wielu osób, co buduje network effect i organiczny wzrost przez zaproszenia.

Usage-based pricing i limity AI

Koszt inferencji modeli nie pozwala na „nielimitowane AI” w niskiej cenie. Dlatego wdrożyliśmy plany Free, Pro i Ultra z twardymi limitami operacji.

Pricing screen: planowanie wartości i limitów na poziomie produktu, zanim request dotrze do płatnego API modelu.

W modelu danych plan trzyma jawne limity, między innymi:

SubscriptionPlan.cs
public sealed class SubscriptionPlan
{
    public string Name { get; set; } = string.Empty;
    public int AiQuizGeneratedLimit { get; set; }
    public int AiFlashcardsGeneratedLimit { get; set; }
    public int AiTutorMessagesLimit { get; set; }
    public int GroupCountLimit { get; set; }
}

Egzekwowanie limitów w backend pipeline

Model biznesowy ma sens tylko wtedy, gdy limity są egzekwowane zanim request trafi do warstwy AI. W Groupnote ten guard działał w pipeline MediatR przez kontrakt IRateLimited.

RateLimitBehavior.cs
public async Task<TResponse> Handle(
    TRequest request,
    RequestHandlerDelegate<TResponse> next,
    CancellationToken ct)
{
    if (request is not IRateLimited limited) return await next();

    var usage = await usageTracker.GetMonthlyUsageAsync(limited.UserId, ct);
    var plan = await plansService.GetCurrentPlanAsync(limited.UserId, ct);

    if (usage.AiQuizGenerated >= plan.AiQuizGeneratedLimit)
        throw new LimitReachedException("AiQuizGeneratedLimit");

    return await next();
}

Stripe integration end-to-end

  1. Frontend inicjuje zakup, backend tworzy Stripe Checkout Session.

  2. Stripe wysyła webhook po sukcesie płatności lub zmianie subskrypcji.

  3. Backend weryfikuje podpis webhooka, aktualizuje UserPlans i odświeża cache.

StripeWebhookController.cs
var stripeEvent = EventUtility.ConstructEvent(
    json,
    request.Headers["Stripe-Signature"],
    stripeOptions.WebhookSecret);

if (stripeEvent.Type == "checkout.session.completed")
{
    var session = stripeEvent.Data.Object as Session;
    await plansService.ActivatePlanAsync(session!.CustomerId, ct);
    await usageCache.InvalidateAsync(session.CustomerId, ct);
}

Efekt: platforma była gotowa na realny billing od pierwszego dnia, a onboarding płatnych userów był w pełni zautomatyzowany.

09 - Co poszło nie tak?

What went wrong: Pułapka "jeszcze jednego ficzera"

Sukces inżynierski, porażka produktowa

Technicznie ten projekt był sukcesem: architektura działała stabilnie, realtime nie gubił znaków, a warstwa AI realizowała złożone flow bez awarii.

Produktowo - nie dowiózł. Nie dlatego, że kod był zły, tylko dlatego, że zbyt długo budowałem w izolacji, zanim zweryfikowałem prawdziwe MVP.

Produkt wyglądał jak gotowy do wypuszczenia: dopracowany onboarding i pełny flow wejścia.

Oryginalna wizja vs rzeczywistość

Początkowy plan był prosty: współdzielone notatki i przycisk „wygeneruj quiz”. To wystarczyło, żeby sprawdzić, czy uczniowie realnie chcą się uczyć z AI.

Zamiast tego doszedłem do poziomu systemu, który był piękny architektonicznie, ale przestał być lekki produktowo.

Co zbudowałem zamiast MVP:

  • pełny edytor blokowy (LaTeX, code blocks, media),

  • Lernie z pamięcią i podstawami RAG,

  • OCR i import zdjęć notatek,

  • generowanie fiszek i pytań otwartych,

  • monetyzacja, tracking zużycia AI i zaproszenia do grup.

Dashboard był rozbudowany i dojrzały wizualnie - ale to nadal nie zastąpiło najważniejszego: szybkiej walidacji z realnym rynkiem.

Founder trap i złudzenie progresu

Każdy kolejny moduł dawał wrażenie postępu. W praktyce to był klasyczny Scope Creep - emocjonalnie satysfakcjonujący, biznesowo niebezpieczny.

  1. „Jeszcze tylko fiszki”.

  2. „Jeszcze tylko pytania otwarte”.

  3. „Jeszcze tylko OCR”.

[ moment prawdy ]

Kod rósł szybko, ale oddalałem się od najważniejszego testu każdego produktu - zderzenia z rynkiem i realnymi użytkownikami.

Ciężar własnej ambicji

W pewnym momencie utrzymanie złożonego ekosystemu zaczęło kosztować więcej energii niż dalszy rozwój. Zwinna deskorolka zamieniła się w fabrykę samochodów bez pierwszego klienta.

10 - Kluczowe wnioski

Key takeaways: Co wyniosłem z Groupnote

Gdybym miał zamknąć ten projekt w kilku konkretnych lekcjach, to dzisiaj wyglądają one tak:

01

Product discipline ponad scope

Prawdziwe MVP musi być małe, szybkie do wypuszczenia i nastawione na walidację, a nie na kompletność funkcji.

02

Pragmatyczna architektura wygrywa z hype

Separacja ASP.NET Core i Python dała bezpieczny System of Record i elastyczną warstwę AI.

03

Leverage infrastruktury to przewaga

Supabase Realtime i PostgreSQL pozwoliły ograniczyć koszt utrzymania i skupić się na wartości produktu.

04

Mindset shift: od kodu do wartości

Najpierw walidacja potrzeby, potem skala architektury. Shipping > perfekcja. Najlepszy kod to ten, który rozwiązuje realny problem biznesowy.

11 - Final Reflection

Refleksja końcowa

Groupnote nie stało się rynkowym sukcesem i nigdy publicznie nie opuściło środowiska deweloperskiego.

Mimo to nie żałuję ani jednej godziny. Ten projekt stał się moim poligonem dla architektury rozproszonej, realtime i AI-native podejścia do produktu.

Przestałem myśleć jak programista. Zacząłem myśleć jak inżynier produktu.

[ moment prawdy ]

Najpierw walidacja potrzeby, potem skala rozwiązania. Bez tego nawet najlepsza architektura staje się ciężarem.

Dziś moje pierwsze pytanie nie brzmi już "Jakiej architektury użyć?", tylko "Jaka jest najmniejsza rzecz, którą mogę wypuścić, aby zweryfikować realną potrzebę?".

Tej lekcji nie da się skopiować z tutoriala - trzeba zbudować własną Gwiazdę Śmierci.