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.
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:
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). 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. 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. 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.
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.
W samym edytorze działał też AI Writing Assistant z podpowiedziami inline (Smart Completions) - akceptacja klawiszem Tab, bez wychodzenia z kontekstu 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.
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:
AI Flashcards: automatyczne generowanie talii fiszek z najważniejszymi definicjami.
AI Quizzes: testy wielokrotnego wyboru do szybkiej weryfikacji wiedzy.
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).
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.
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.
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.
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_apiInfrastructure 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.
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.
// 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;
}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; }
}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
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`.
[
{
"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.
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.
Uczeń edytuje TextBlock - zmiana trafia od razu do lokalnego stanu React.
Po 1 sekundzie ciszy frontend wysyła pojedynczy UPDATE dla konkretnej notatki.
Supabase rozgłasza zmianę do wszystkich klientów subskrybujących dokument.
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();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 } },
});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
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)
Frontend wysyła request do API .NET.
Backend sprawdza limity planu w Redis.
Handler pobiera kontekst notatki (jsonb) i deleguje call do serwisu Python przez HTTP.
Python zwraca wynik jako DTO/JSON, a .NET zapisuje rezultat.
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);
}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.
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);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.
{
"userId": "usr_123",
"memoryContext": [
"User prefers game-based examples",
"User struggles with memorizing dates"
],
"tone": "Academic",
"noteContext": "..."
}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.
W modelu danych plan trzyma jawne limity, między innymi:
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.
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
Frontend inicjuje zakup, backend tworzy Stripe Checkout Session.
Stripe wysyła webhook po sukcesie płatności lub zmianie subskrypcji.
Backend weryfikuje podpis webhooka, aktualizuje UserPlans i odświeża cache.
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.
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.
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.
„Jeszcze tylko fiszki”.
„Jeszcze tylko pytania otwarte”.
„Jeszcze tylko OCR”.
[ moment prawdy ]
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 ]
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.
