A reading tracker built as a sandbox for exploring architectural and infrastructure needs. Users register, browse a catalog, add books to their library, and track reading progress across ebooks, paperbacks, and audiobooks.
Storygame is intentionally a real, working application — not a tutorial or a minimal example. The goal isn't the reading tracker itself; it's the system around it.
Building good architectural tools — a custom message queue, an event store, a cache layer, a request throttler — only makes sense once you have something that genuinely needs them. Without a real workload, design decisions are speculation. Storygame is the workload: enough domain, enough flows, and enough moving parts to surface concrete problems that justify concrete solutions.
When something hurts here — a slow query, a fragile coordination point, a missing observability hook — that pain becomes the brief for the next library or service to build.
- Not a polished consumer product. UI gaps, hardcoded data, and TODO comments are intentional; smoothing them over too early hides the problems worth solving.
- Not a reference implementation. Some choices are deliberately naive so they can be replaced later (in-memory session storage, in-memory catalog, an in-process event dispatcher).
- Not stable. APIs, schemas, and project layout change as the architecture evolves.
The application currently supports:
- Passwordless authentication — registration with email verification, then login via one-time codes. Emails are stored in an in-process inbox you can read at
/mailinstead of being sent out. - Session management — server-side session store with logout, expiration, user-agent binding, and CSRF protection on top of cookie auth.
- Book catalog — a fixed catalog of titles, each available as text edition, audiobook, or both. Filterable by title and format.
- Personal library — users add catalog books or custom books in a chosen format (ebook, paperback, audiobook).
- Progress tracking — start tracking a book, update the current page or minute, see percent complete and finished state.
- Statistics projections — every progress update fans out to daily, weekly, monthly, and yearly counters via event handlers.
- Traffic simulator — a console app that spins up multiple user scenarios (binge reader, casual sampler, abandoner, audiobook listener) against a running instance.
Backend — ASP.NET Core 10, MongoDB, Serilog. Hand-rolled CQRS with a dispatcher and Scrutor-based handler registration. Events are dispatched in-process to multiple handlers (for example, the statistics projectors).
Frontend — React 19 with React Router 7 in SPA mode, TypeScript, Tailwind CSS 4.
Infrastructure — Docker Compose runs MongoDB, the backend, the frontend, and an nginx reverse proxy that terminates TLS and routes /api/* to the backend.
The solution is split into small projects by responsibility:
Storygame.Catalog,Storygame.Library,Storygame.Tracking,Storygame.Users— domain logic, each with its commands, queries, and events.Storygame.Cqrs— dispatcher, command/query/event interfaces.Storygame.Storage— MongoDB repositories implementing domain interfaces.Storygame.Web— HTTP endpoints, auth, rate limiting.Storygame.Contracts.WebApi— DTOs and request/response shapes.Storygame.Client— typed HTTP client used by the traffic simulator and integration tests.Storygame.TrafficSimulator— load-generating scenarios.
Tests live in Storygame.Tests.Unit (handlers in isolation, mocked repositories) and Storygame.Tests.Integration (end-to-end through the typed client against a WebApplicationFactory).
This is the recommended way to run the full stack — four containers behind nginx with HTTPS.
- Docker and Docker Compose
openssl(ships with Git for Windows, macOS, and most Linux distributions)
The app uses HTTPS-only cookies, so the proxy needs a cert. Run this once from the project root:
mkdir certs
openssl req -x509 -newkey rsa:4096 \
-keyout certs/key.pem \
-out certs/cert.pem \
-days 365 \
-nodes \
-subj "/CN=localhost"docker compose up --buildThe first build pulls the .NET SDK image and installs npm packages — a few minutes. Subsequent builds are fast.
Go to https://localhost. Accept the certificate warning (in Chrome, type thisisunsafe anywhere on the warning page to bypass it).
docker compose downMongoDB data lives in a named volume (mongodb_data) and survives restarts. To wipe it:
docker compose down -vUseful when iterating on backend or frontend code without rebuilding containers.
- .NET 10 SDK
- Node.js 20+
- MongoDB running locally on
localhost:27017(Docker works:docker run -p 27017:27017 mongo:8)
cd src/Storygame.Web
dotnet runThe API serves on http://localhost:5263.
cd src/client
npm install
npm run devThe dev server runs on http://localhost:5173 and proxies /api/* to the backend.
In Development mode, all "sent" emails are kept in memory and exposed at GET /api/mail/{email}. The frontend has a UI for this at /mail. Use it to grab verification codes and login confirmation keys.
dotnet test src/Storygame.Tests.Unit/Storygame.Tests.Unit.csproj
dotnet test src/Storygame.Tests.Integration/Storygame.Tests.Integration.csprojThe same tests run on every push and pull request via GitHub Actions (.github/workflows/dotnet.yml).
With the backend running on HTTPS (default https://localhost:7121), run:
cd src/Storygame.TrafficSimulator
dotnet runThe simulator registers fake users and runs a mix of reading scenarios against the API.
- An IDE with C# support: Visual Studio, Rider, or VS Code with the C# Dev Kit.
- Node.js for the frontend toolchain (Vite, TypeScript, Tailwind).
- Optional: MongoDB Compass or
mongoshfor inspecting the database.
Issues and pull requests are welcome. Because the project's purpose is exploring architectural needs rather than shipping features, the most valuable contributions are usually:
- Identifying friction points or scaling concerns in the current code.
- Proposing or prototyping infrastructure pieces (cache, queue, event store, observability) that the app could plausibly use.
- Adding scenarios to the traffic simulator that stress new parts of the system.
Feature work on the reading-tracker side is welcome too, as long as it gives the architecture something concrete to react to.