KvizHub is a modular web application for creating and solving quizzes, tracking personal results, and comparing scores on leaderboards. The system follows a microservices design where each backend service owns its data and logic, and the frontend communicates exclusively through an API gateway. The technology stack consists of React + TypeScript on the frontend and .NET 8 Web APIs on the backend (Identity, Quiz, Results), with each service connected to its own SQL Server database. The gateway centralizes routing, logging, and authentication pass-through so that services remain focused on their domains.
Local development requires the .NET 8 SDK, Node.js version 18 or later, and SQL Server (LocalDB or full). Start the three backend services first, then the gateway, and finally the frontend. Each service can run independently and binds to a fixed port for clarity during development.
To run the system locally:
cd backend/services/identity dotnet run --urls "http://localhost:5001"
cd backend/services/quiz dotnet run --urls "http://localhost:5002"
cd backend/services/results dotnet run --urls "http://localhost:5003"
Then start the gateway:
cd backend/gateway dotnet run --urls "http://localhost:5000"
The gateway listens on port 5000 and forwards requests to downstream services without altering authentication headers. Schema and seed data are applied on service startup when the ApplySqlScripts flag is enabled in configuration, which makes the system reproducible in a clean environment. During development, maintenance SQL can be executed when test submissions must be cleared; for example, a helper script is used in the Results database to delete submissions and related answers for a specific user so that scoring scenarios can be replayed.
The frontend lives in the frontend directory. Install dependencies with npm, copy .env.example to .env, and set VITE_API_BASE_URL to the gateway address (typically http://localhost:5000). Start the dev server with npm run dev. The app will be available at http://localhost:5173 by default.
The Identity service handles registration, login, token issuing, and profile retrieval. The database schema centers on a Users table (Id, Username, Email, PasswordHash, AvatarUrl, CreatedAt). Passwords are stored exclusively as BCrypt hashes. A RefreshTokens table supports session extension and rotation with fields for token value, expiration, revocation timestamp, and linkage to replacement tokens.
The AuthController exposes endpoints for POST /api/auth/register, POST /api/auth/login, GET /api/auth/me, and POST /api/auth/refresh. Registration enforces uniqueness on username and email, hashes the password, and persists the user. Login verifies credentials against the hash and issues a JWT with standard claims (NameIdentifier, Name, Email). The token is self-contained so that other services validate it without calling back to Identity. The profile endpoint reads claims and returns a minimal user payload after confirming database presence. The refresh endpoint issues a new access token and rotates the refresh token when a valid, unrevoked refresh token is presented.
The AppDb context derives from DbContext and exposes DbSet<User> and DbSet<RefreshToken>. Conventional EF Core configuration is used to map tables and keys. In OnModelCreating, unique indexes on Username and Email prevent duplicate accounts. Configuration settings (JWT key, issuer, audience, token lifetimes) live in appsettings.json and are injected into token creation logic.
The Quiz service provides content management. Its schema includes Quizzes, Questions, and AnswerOptions. A quiz stores metadata (title, description, category, difficulty, time limit, creator, created-at). Questions reference their parent quiz, carry text, order, type, and point value. Answer options belong to a question and mark which options are correct. The model supports single- and multiple-choice questions by comparing option identifier sets rather than relying on a single flag.
The QuizzesController offers endpoints for discovery and administration. GET /api/quizzes lists quizzes with optional filtering by category, difficulty, and free-text search. Results are projected to a compact shape that includes a computed questionsCount to keep list payloads small. GET /api/quizzes/{id} returns a full quiz with questions and options, which simplifies the player UI. Administrative endpoints (POST, PUT, DELETE /api/quizzes) enable content creation and updates by authorized users. Seed data ships one or more sample quizzes so the system is usable immediately.
The AppDb context exposes DbSet<QuizEntity>, DbSet<Question>, and DbSet<AnswerOption>. Relations are configured so that deleting a quiz cascades to questions and options. In OnModelCreating, constraints enforce non-null text fields, reasonable max lengths, and deterministic ordering by the Order field. This keeps data coherent and improves query predictability in the UI.
The Results service owns quiz attempts, scoring, and leaderboards. When a player starts a quiz, POST /api/results/quiz-sessions creates a submission record and fetches the exact quiz definition from the Quiz service. The service serializes that definition as a snapshot and stores it with the submission. This design guarantees that scoring refers to the content the player actually saw, independent of later edits. The Submissions table records QuizId, UserId, StartedAt, FinishedAt, Score, Percentage, and SnapshotJson for auditability. Per-question data is stored in SubmissionAnswers with QuestionId, SelectedOptionIdsJson, IsCorrect, and TimeSpentMs to enable later analytics.
During submission, the client sends answers to POST /api/results/quiz-sessions/{id}/submit with { submissionId, answers }, where answers is an array like { questionId: number, selectedOptionIds: number[] }[]. The service reconstructs the correct option sets from the snapshot and applies a deterministic rule: the set of selected option ids must equal the set of correct option ids for the question to be counted correct. Score and percentage are computed and stored, and a concise summary is returned to the client. Additional read endpoints include GET /api/results/me for a users history, GET /api/results/leaderboard?quizId= for per-quiz rankings, GET /api/results/global for aggregated stats, and GET /api/results for admin filtering across users and quizzes.
The AppDb context exposes DbSet<Submission> and DbSet<SubmissionAnswer>. Foreign keys enforce that answers belong to submissions and that submissions target an existing quiz id. Indexes on (UserId, StartedAt) and (QuizId, Percentage) optimize common history and leaderboard queries. The service uses IHttpClientFactory to call the Quiz service at start-time for the snapshot; timeouts and simple retries are configured to keep the start flow robust under transient conditions.
The Ocelot gateway is the only network surface exposed to the frontend. Its configuration file ocelot.json maps the upstream routes that begin with http://localhost:5000 and forwards /api/auth to Identity (5001), /api/quizzes to Quiz (5002), and /api/results to Results (5003). Ocelot and Serilog so that every request is logged both to the console and to rotating files. The reason for placing Serilog at the gateway is to record a single, authoritative trail of external traffic regardless of which downstream service ultimately handles it. Centralizing routing also allows consistent cross-cutting policy, such as rate limiting or header transformation, to be applied later without modifying service code. Because the gateway forwards the Authorization header, downstream services can perform standard JWT validation using the shared secret configured in their appsettings.json. The consequence of this design is that services remain internally simple: they trust the gateway to provide a normalized entry point and they focus exclusively on their domain logic.
The frontend is organized around features and an explicit service layer to isolate network concerns from presentation. The file src/services/api.ts constructs an Axios instance with its baseURL taken from the VITE_API_BASE_URL environment variable. It registers a request interceptor that reads the JWT token from localStorage and attaches it to the Authorization header. The functions AuthApi.login and AuthApi.register call the Identity endpoints and return the raw responses so that the calling components can decide what to do next. The function QuizApi.list queries the quizzes endpoint with optional parameters to support searching and filtering, and QuizApi.get retrieves the full representation of a single quiz. ResultsApi.start begins a session by posting the quiz id to the Results service, which responds with a submission identifier and the snapshot necessary to render the questions; ResultsApi.submit posts the users answers along with the submission identifier and receives the computed score and percentage.
The components in src/features reflect these flows. Login.tsx renders a form, calls AuthApi.login, and stores the token so that later requests include proper credentials. Register.tsx enables account creation by calling AuthApi.register and redirecting to the login view if successful. Quizzes.tsx requests the list of available quizzes on mount and renders minimal metadata about each item so that the user can choose what to play. Play.tsx receives the quiz identifier from the route, calls ResultsApi.start to obtain a submission id and the snapshot, maintains a simple local state of selected options for each question, decreases an internal countdown timer every second, and calls ResultsApi.submit when the user finishes, showing the returned summary. This organization is intentional: components remain focused on rendering and local interaction state, while the service layer encapsulates all network behavior and token management.
- Database bootstrap and seeding: Schema and seed scripts are applied on service startup when
ApplySqlScripts=true. This reduces set-up friction but requires attention in environments where data persistence is desired across runs. - Results maintenance: A helper SQL script is used to remove submissions and answers for specific test users in the Results database. This keeps leaderboards and personal histories consistent when replaying scenarios during development.
- Snapshot-based scoring: Scoring relies on the serialized quiz snapshot captured at session start. The approach guarantees auditability but requires careful versioning if quiz structures evolve; migrations should preserve the ability to read older snapshots.
- Answer schema contract: The submit endpoint expects
answers: { questionId, selectedOptionIds[] }[]. Frontend types must reflect this shape to avoid runtime or compile-time mismatches. - Gateway routing: The gateway must be running for the frontend to access services. If services are moved to different ports or hosts, update
ocelot.jsonand the frontend.envaccordingly.