The Mind Block backend is a NestJS HTTP API. This document is a hand-written overview of the surface area; the generated, always-current contract is the Swagger UI the app serves at runtime.
| Environment | Base URL | Swagger UI |
|---|---|---|
| Local | http://localhost:3000 |
http://localhost:3000/api |
| Hosted | https://mindblock-webaapp.onrender.com |
https://mindblock-webaapp.onrender.com/api |
The Swagger document is built in backend/src/main.ts and mounted at /api.
All request and response bodies are JSON. Send Content-Type: application/json
on any request with a body.
Authentication is a JWT bearer token:
Authorization: Bearer <accessToken>JwtAuthMiddleware is applied globally in backend/src/app.module.ts and
excludes only these public prefixes:
/auth/*/api(Swagger UI)/docs/health
Every other route requires a valid token. Obtain one from POST /auth/signIn,
POST /auth/stellar-wallet-login, or the Google endpoint, and refresh it with
POST /auth/refreshToken.
Handlers that use the @ActiveUser() decorator read the user from the verified
token, so you do not pass a user ID for those endpoints.
A global ValidationPipe runs with whitelist: true,
forbidNonWhitelisted: true, and transform: true. Unknown properties are a
400, not a silent strip:
{
"statusCode": 400,
"message": ["property nickname should not exist"],
"error": "Bad Request"
}AllExceptionsFilter catches everything, so non-HTTP exceptions still return
structured JSON rather than a raw stack trace. Every request is stamped with a
correlation ID by CorrelationIdMiddleware; include it when reporting a bug.
| Status | Meaning |
|---|---|
| 400 | Validation failure or malformed payload. |
| 401 | Missing, expired, or invalid bearer token. |
| 403 | Authenticated but not permitted (admin routes, shutdown, bad admin key). |
| 404 | Resource does not exist. |
| 429 | Rate limit exceeded (@nestjs/throttler). |
| 500 | Unhandled server error. |
Throttling is applied per route where it matters, for example
GET /auth/stellar-wallet-nonce allows 5 requests per minute and
POST /analytics/track allows 20 per minute.
List endpoints that paginate accept:
| Query | Type | Default |
|---|---|---|
page |
positive integer | 1 |
limit |
positive integer | 10 |
Paginated responses carry data, a meta block (itemsPerPage, totalItems,
currentPage, totalPages) and a links block (first, last, current,
previous, next).
CORS is currently open (origin: '*') with GET, POST, PUT, DELETE, and
OPTIONS allowed, and Content-Type and Authorization accepted as headers.
| Area | Base path | Auth |
|---|---|---|
| Root | / |
Public |
| Auth | /auth |
Public |
| Google auth | /auth/google-authentication |
Public |
| Health | /health |
Public (/health/detailed needs an admin key) |
| Users | /users |
Bearer |
| Puzzles | /puzzles |
Bearer |
| Categories | /categories |
Bearer |
| Game sessions | /game-sessions |
Bearer or guest ID |
| Challenge attempts | /challenge-attempts |
Bearer |
| Progress | /progress |
Bearer |
| Daily quest | /daily-quest |
Bearer |
| Streaks | /streaks |
Bearer |
| Analytics | /analytics |
Bearer (retention is admin-only) |
| Blockchain | /blockchain |
Bearer |
| Admin IQ questions | /admin/iq-questions |
Bearer + ADMIN role |
| Method | Path | Description |
|---|---|---|
GET |
/ |
Service greeting from AppController. |
All routes are public.
| Method | Path | Description |
|---|---|---|
POST |
/auth/signIn |
Email and password sign-in. |
POST |
/auth/refreshToken |
Exchange a refresh token for a new access token. |
POST |
/auth/guest-session |
Create a 15-minute guest session. Returns 201. |
GET |
/auth/guest-session/:sessionId/status |
Guest session status and expiry. |
POST |
/auth/guest-session/:sessionId/hint |
Consume a guest hint. Max 2; 403 once exhausted. |
POST |
/auth/convert-guest |
Convert a guest session into a real account. |
GET |
/auth/stellar-wallet-nonce?walletAddress=G... |
Nonce to sign. Expires in 5 minutes. Throttled to 5/min. |
GET |
/auth/stellar-wallet-nonce/status |
Inspect an issued nonce. |
POST |
/auth/stellar-wallet-login |
Log in with a signed nonce. |
POST |
/auth/forgot-password |
Send a password reset mail. |
POST |
/auth/reset-password/:token |
Set a new password using the emailed token. |
POST |
/auth/google-authentication |
Exchange a Google ID token for app credentials. |
{
"email": "player@example.com",
"password": "Password123!"
}Returns the access token (and refresh token) used for every protected route.
401 on invalid credentials.
{ "refreshToken": "some-refresh-token" }400 when the refresh token is invalid or expired.
Three steps:
# 1. Ask for a nonce
curl "http://localhost:3000/auth/stellar-wallet-nonce?walletAddress=GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A"
# -> { "nonce": "stellar_nonce_...", "expiresAt": 1693123756789 }
# 2. Sign the nonce in the wallet (Freighter, xBull, Albedo)
# 3. Exchange the signature for a token
curl -X POST http://localhost:3000/auth/stellar-wallet-login \
-H "Content-Type: application/json" \
-d '{
"walletAddress": "GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A",
"signature": "base64SignatureString==",
"nonce": "stellar_nonce_1693123456789_abc123_BTODB4A",
"publicKey": "GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A"
}'
# -> { "accessToken": "..." }400 for a malformed address or an expired nonce, 401 for a bad signature.
{
"guestSessionId": "guest_...",
"email": "player@example.com",
"password": "Password123!",
"walletAddress": "G..."
}email/password and walletAddress are both optional individually; supply
whichever identity the player is upgrading to.
// POST /auth/forgot-password
{ "email": "player@example.com" }
// POST /auth/reset-password/:token
{ "password": "NewPassword123!" }Requires working SMTP configuration; see ENVIRONMENT.md.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Basic liveness plus status summary. |
GET |
/health/live |
Kubernetes liveness probe. |
GET |
/health/ready |
Readiness probe. 403 when dependencies are unhealthy. |
GET |
/health/detailed |
Full dependency detail. Requires the x-admin-key header to match ADMIN_HEALTH_KEY. |
During graceful shutdown these endpoints return 403 so load balancers drain
traffic before the process exits.
curl -H "x-admin-key: $ADMIN_HEALTH_KEY" http://localhost:3000/health/detailedBearer token required.
| Method | Path | Description |
|---|---|---|
GET |
/users?page=1&limit=10 |
Paginated user list. |
GET |
/users/:id |
Single user. |
GET |
/users/:id/xp-level |
XP total and derived level. |
POST |
/users |
Create a user. |
PATCH |
/users/:id |
Update a user profile. |
DELETE |
/users/:id |
Delete a user. |
POST /users accepts email, username, fullname, password, userRole,
walletAddress, publicKey, provider, and googleId; all are optional at the
DTO level, so supply the identity fields relevant to the account type.
PATCH /users/:id accepts name, username, and avatar.
| Method | Path | Description |
|---|---|---|
POST |
/puzzles |
Create a puzzle. 201 on success. |
GET |
/puzzles |
List puzzles, filterable. |
GET |
/puzzles/daily-quest |
Puzzles making up today's daily quest. |
GET |
/puzzles/:id |
Single puzzle. |
Create body:
{
"question": "Which data structure gives O(1) average lookup?",
"options": ["Array", "Hash map", "Linked list", "Binary tree"],
"correctAnswer": "Hash map",
"difficulty": "INTERMEDIATE",
"categoryId": "8e2f...",
"points": 20,
"timeLimit": 60,
"explanation": "Hashing gives constant-time average access."
}points and explanation are optional. 400 if the category does not exist or
is inactive.
List filters:
| Query | Description |
|---|---|
categoryId |
Restrict to one category. |
difficulty |
One of the PuzzleDifficulty values: BEGINNER, INTERMEDIATE, ADVANCED, EXPERT. |
| Method | Path | Description |
|---|---|---|
POST |
/categories |
Create a category. |
GET |
/categories?isActive=true |
List categories, optionally filtered by active state. |
{
"name": "Logic",
"description": "Deductive reasoning puzzles",
"icon": "brain",
"isActive": true
}Only name is required.
A game session is one play-through: a set of challenges, a score, and XP.
Authenticated players are identified by their token; guests pass guestId.
| Method | Path | Description |
|---|---|---|
POST |
/game-sessions |
Start a session. |
GET |
/game-sessions |
Sessions for the current user. |
GET |
/game-sessions/active |
The user's currently active session. |
GET |
/game-sessions/:id?guestId=... |
Fetch one session. :id must be a UUID. |
PATCH |
/game-sessions/:id/status?guestId=... |
Update status, score, and XP. |
Create body:
{
"challengeCount": 10,
"difficulty": "INTERMEDIATE",
"selectedCategories": ["8e2f...", "b41c..."],
"guestId": "guest_..."
}Only challengeCount is required; guestId is for unauthenticated play.
Status update body (GameSessionStatus: CREATED, ACTIVE, PAUSED, COMPLETED, EXPIRED, ABANDONED):
{ "status": "COMPLETED", "score": 80, "xpEarned": 120 }Response shape: id, status, challengeCount, currentChallenge, score,
xpEarned, createdAt, updatedAt.
The execution ledger: one record per challenge a player takes on.
| Method | Path | Description |
|---|---|---|
POST |
/challenge-attempts |
Begin an attempt. |
POST |
/challenge-attempts/submit |
Submit an answer. |
PATCH |
/challenge-attempts/hint |
Consume a hint. |
PATCH |
/challenge-attempts/reveal |
Reveal the solution. |
PATCH |
/challenge-attempts/:id/expire |
Mark an attempt expired. |
GET |
/challenge-attempts/:id |
One attempt. |
GET |
/challenge-attempts/user/:userId |
All attempts by a user. |
GET |
/challenge-attempts/session/:sessionId |
All attempts in a session. |
// POST /challenge-attempts
{ "userId": "...", "challengeId": "...", "sessionId": "..." }
// POST /challenge-attempts/submit
{ "attemptId": "...", "answer": "Hash map", "timeSpent": 24 }
// PATCH /challenge-attempts/hint and /reveal
{ "attemptId": "..." }timeSpent is in seconds. Scoring is decided server-side.
| Method | Path | Description |
|---|---|---|
GET |
/progress?page=1&limit=10 |
Paginated attempt history for the current user. |
GET |
/progress/stats |
Overall statistics. |
GET |
/progress/category/:id |
Statistics for one category. |
POST |
/progress/submit |
Submit an answer and record progress. |
History entries contain id, puzzleId, question, userAnswer,
isCorrect, pointsEarned, timeSpent, attemptedAt, and categoryId.
GET /progress/stats returns totalAttempts, totalCorrect, accuracy,
totalPointsEarned, and totalTimeSpent. The category variant returns
categoryId, categoryName, totalAttempts, correctAnswers, and accuracy.
// POST /progress/submit
{
"userId": "...",
"puzzleId": "...",
"categoryId": "...",
"userAnswer": "Hash map",
"timeSpent": 24
}| Method | Path | Description |
|---|---|---|
GET |
/daily-quest |
Today's quest with its puzzles. |
GET |
/daily-quest/status |
Progress counters only. |
POST |
/daily-quest/complete |
Finalize the quest, award bonus XP, update the streak. |
POST /daily-quest/complete is idempotent: repeated calls do not duplicate
rewards. It returns success, message, bonusXp, totalXp, a streak
object (currentStreak, longestStreak, lastActivityDate), and completedAt.
400 if the quest is not fully solved, 404 if no quest exists for today.
Quest payloads carry id, questDate, totalQuestions, completedQuestions,
isCompleted, pointsEarned, createdAt, completedAt, and puzzles. Puzzle
entries in a quest never include the correct answer.
| Method | Path | Description |
|---|---|---|
GET |
/streaks |
The current user's streak record. |
POST |
/streaks/update |
Recalculate the streak after a daily quest completion. |
The user is taken from the bearer token; 401 when it is missing or invalid.
| Method | Path | Description |
|---|---|---|
GET |
/analytics/ping |
Module health check. |
POST |
/analytics/track |
Record an event. Throttled to 20 requests per minute. |
GET |
/analytics/funnel/onboarding?start=&end= |
Onboarding funnel over a date range. |
GET |
/analytics/users/retention?start=&end=&granularity= |
Retention curve. Admin role required. |
// POST /analytics/track
{ "eventType": "onboarding_started", "userId": "..." }
// -> { "success": true }start and end are ISO date strings. Non-admin callers get 403 from
AnalyticsAdminGuard on the retention endpoint.
| Method | Path | Description |
|---|---|---|
GET |
/blockchain |
Blockchain module status. |
POST |
/blockchain/wallet/link |
Link a Stellar wallet to the signed-in account. |
{ "walletAddress": "GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A" }Requires a bearer token whose user holds the ADMIN role; enforced by
RolesGuard.
| Method | Path | Description |
|---|---|---|
POST |
/admin/iq-questions |
Create an IQ question. |
DELETE |
/admin/iq-questions/:id |
Delete an IQ question. |
These handlers are stubs today and return acknowledgement messages.
# 1. Sign in
TOKEN=$(curl -s -X POST http://localhost:3000/auth/signIn \
-H "Content-Type: application/json" \
-d '{"email":"player@example.com","password":"Password123!"}' \
| python -c "import json,sys; print(json.load(sys.stdin)['accessToken'])")
# 2. Call a protected route
curl http://localhost:3000/progress/stats -H "Authorization: Bearer $TOKEN"
# 3. Start a game session
curl -X POST http://localhost:3000/game-sessions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"challengeCount":10,"difficulty":"INTERMEDIATE"}'backend/src/endpoint.http and backend/http/ hold ready-made requests you can
fire from the VS Code REST Client extension.
Swagger is generated from decorators, so a new endpoint appears at /api
automatically once you annotate it with @ApiOperation and @ApiResponse. When
you add or change a route, update the matching table here in the same pull
request.