Skip to content
Full stackOne .NET API · web, admin and mobile clients

Your material, turned into practice.

Turns any document, video or podcast into summaries, flashcards and quizzes, scheduled with FSRS-4.5 — one .NET API behind web, admin and mobile apps.

Personal product · toto-study.com · 2025 – 2026 · source on GitHub

Toto Study walkthrough: pasting a YouTube link into the AI summarizer and generating a summary, flashcards and quizzes.
4
DeployablesAPI, web, admin, mobile
234
Document typesincl. OCR for scans
9
LLM providersswitchable in settings
1,512
Backend testsxUnit, all passing

Under the hood

Clean Architecture + CQRS

Dependencies point inward: API → Application → Domain ← Infrastructure. Handlers depend only on interfaces, never on a vendor.

Fig. 1

Dependencies point inward; the pipeline hangs off Infrastructure

Solid arrows are calls, dashed are dependency inversion. The AI key arrives in X-AI-* headers and lives only for that request, so tokens stream straight back as SSE.

Fig. 2

The stack, layer by layer

L01APIThin controllers and background workersJWT user id → MediatR request → BaseResponse<T>Only CloudFront gets in: a shared-secret header, else 403In-process workers: a one-at-a-time Whisper queue, push reminders, embeddings, exports, expiry sweepsL02ApplicationOne feature slice per folderMediatR requests with their handlersFluentValidation runs before every handlerDeclares the service interfacesL03DomainEntities and repository contractsPOCO entities, no framework referencesRepositories behind one IUnitOfWorkL04InfrastructureEF Core and service implementationsImplements every interface declared aboveWired up in a single AddInfrastructure()

Arrows are compile-time references. Domain references nothing.

Captions from the cloud

It worked on a laptop. On AWS, YouTube asked it to sign in.

From the production box, every yt-dlp caption request came back “Sign in to confirm you’re not a bot”. The fix: stop looking like a server.

What counts as a bot

Where it comes from

An AWS address — cloud IP ranges are public, so it reads as a script.

Instead: Residential proxies instead of the server's own IP.

Who is asking

Nobody — no session cookies, so YouTube asks it to sign in.

Instead: Cookies exported from real signed-in browsers.

How often

Too many requests from one IP or session — HTTP 429 or 403.

Instead: Round-robin over the pool; a flagged pair sits out 5 minutes.

Fig. 3

One caption request, from a cloud IP

Each failure is classified, so the retry swaps only what was blamed: a dead proxy costs a proxy, a bot check costs a cookie set. Five attempts, then Whisper.

All systems degrade gracefully

Degrade,
don’t fail.

Every dependency has a fallback. Slower, never broken.

  1. Redis is downPostgres serves the cacheproduction runs without Redis
  2. A video has no captionsWhisper transcribes the audio
  3. Semantic search failskeyword search answers
  4. An export job stallsit is requeued after 30 minutes

How it works

From upload to review.

  1. 1

    Upload

    Hashed for duplicates, stored in S3.

  2. 2

    Extract

    Parsed by type; video via captions, else a queued Whisper job.

  3. 3

    Embed

    Chunks indexed in pgvector.

  4. 4

    Generate

    Streamed over SSE, with citations.

  5. 5

    Review

    Scheduled by FSRS-4.5 on every client.