Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 274 additions & 43 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,32 @@ authoritative specification.
## 1. High-level shape

```text
+---------------------------+
| Frontend (Next.js 16) |
| App Router, React 19, |
| Redux Toolkit, Tailwind |
+-------------+-------------+
| HTTPS / JSON
v
+---------------------------+ +--------------+
| Backend (NestJS 11) |<-------->| PostgreSQL |
| REST API, JWT auth, | TypeORM +--------------+
| domain modules, Swagger |
+------+-------------+------+ +--------------+
| |<--------------->| Redis |
| ioredis +--------------+
v
+-----------------------+
| Stellar / Soroban |
| wallet auth, rewards |
+-----------------------+
^
| Freighter, xBull, Albedo
+-----------------------+
| Player's wallet |
+-----------------------+
+---------------------------+
| Frontend (Next.js 16) |
| App Router, React 19, |
| Redux Toolkit, Tailwind |
+-------------+-------------+
| HTTPS / JSON
v
+---------------------------+ +--------------+
| Backend (NestJS 11) |<-------->| PostgreSQL |
| REST API, JWT auth, | TypeORM +--------------+
| domain modules, Swagger |
+---+----------+-------+----+ +--------------+
| |<------------------->| Redis |
| | ioredis +--------------+
+----------+-----+ +-+---------------------+-+
v v v v
+------------------+ +---------------------+ +------------------------+
| Stellar wallet | | AI Services | | Blockchain Service |
| auth & linking | | (planned — no code | | (Soroban contract, |
| (off-chain) | | in the repo yet) | | on-chain rewards) |
+---------+--------+ +---------------------+ +-----------+------------+
^ |
| Freighter, xBull, Albedo v
+------------------+ +----------------------+
| Player's wallet | | StarkNet / Soroban |
+------------------+ +----------------------+
```

The frontend never talks to the database. All scoring, verification, and reward
Expand Down Expand Up @@ -161,7 +163,174 @@ losing progress.

---

## 4. Frontend
## 4. Authentication architecture

Four ways to sign in exist today, all funneling into the same JWT machinery.

| Method | Route(s) | Mechanism |
| ------ | -------- | --------- |
| Email + password | `POST /auth/register`, `POST /auth/signIn` | bcryptjs (salt rounds 10); unknown email and wrong password return the identical error, with a dummy-hash compare to flatten timing differences. Both routes throttled to 5 req/min/IP. |
| Google OAuth | `POST /auth/google-authentication` | The client obtains a Google ID token; the backend verifies it with `google-auth-library`'s `OAuth2Client.verifyIdToken` and matches or auto-provisions the user by `googleId`. No server-side redirect flow; the installed `passport-google-oauth20` package is currently unused. |
| Stellar wallet signature | `GET /auth/stellar-wallet-nonce`, `POST /auth/stellar-wallet-login` | Single-use nonce (format `stellar_nonce_{timestamp}_{random}_{suffix}`, 5-minute TTL). The client signs `Stellar Signed Message:\n{nonce}` with their wallet; the backend hashes it with SHA-256 and verifies the ed25519 signature via `stellar-sdk`. Unknown wallets are auto-registered (`stellar_user_{last6}`, provider `wallet`). Returns an access token only — no refresh token or session row. |
| Guest session | `POST /auth/guest-session`, `POST /auth/convert-guest` | In-memory session (`guest_{uuid}`) valid 15 minutes with at most 2 hints (`POST /auth/guest-session/:sessionId/hint`). Conversion flips the role to USER in place. |

### Tokens and sessions

- **JWT**: access tokens default to 3600 s, refresh tokens to 7 days, signed
with `JWT_SECRET` and audience/issuer claims; `JwtStrategy` validates every
Bearer token.
- **Refresh rotation**: only the SHA-256 hash of the refresh token is stored,
in the `Session` entity. Refreshing invalidates the old session row and mints
a new pair; `logout` deactivates one session, `logout-all` deactivates all.
- **Redis blacklist**: `JwtAuthMiddleware` consults a per-token blacklist key
before verifying. Nothing writes those keys yet — DB session rows are the
effective revocation mechanism today.
- **Password reset**: a random 32-byte hex token is stored hashed with a
1-hour expiry and delivered by SMTP (`MAIL_*` configuration).
- **Rate limiting**: a global 10 req/min throttler applies everywhere, with
stricter per-route limits on credential endpoints.

> **Known limitation.** Guest sessions, conversion state, and login nonces live
> in in-memory Maps: they do not survive restarts and are not safe across
> multiple instances. Moving them to Redis is straightforward when needed.

---

## 5. Game-session architecture

A `GameSession` is one play-through. Its lifecycle is a strict state machine —
transitions live in the `SESSION_TRANSITIONS` map in
`game-sessions.service.ts`, and an illegal transition is rejected with a `400`.

```text
CREATED ──> ACTIVE ──> PAUSED ──> ACTIVE
│ │ │
│ │ └──> ABANDONED / EXPIRED
│ └──> COMPLETED / ABANDONED / EXPIRED
└──> (COMPLETED, EXPIRED, ABANDONED are terminal)
```

Status values (`GameSessionStatus`): `CREATED`, `ACTIVE`, `PAUSED`,
`COMPLETED`, `EXPIRED`, `ABANDONED`.

The owning client drives transitions via `PATCH /game-sessions/:id/status`
(ownership checked, including guest play through a `guestId`). On the first
`ACTIVE` the backend stamps `startedAt`; terminal states stamp `completedAt`.
When a session completes, `SessionSummaryProvider.buildCompletionStats`
recomputes everything from persisted `ChallengeAttempt` rows — total score, XP
(`XP_PER_SCORE_POINT = 1`, a documented placeholder), accuracy percentage,
time spent, and per-category performance stored as jsonb — then updates the
streak and evaluates reward eligibility. Client-supplied score/XP values are
fallbacks used only when a session has zero tracked attempts. A scheduled
helper, `expireIdleSessions(30)`, sweeps stale `ACTIVE`/`PAUSED` sessions to
`EXPIRED`.

---

## 6. Challenge lifecycle

Each answered puzzle is a `ChallengeAttempt` with status `AttemptStatus`:
`STARTED`, `SUBMITTED`, `CORRECT`, `INCORRECT`, `EXPIRED`. (`SUBMITTED` is
defined but never assigned; grading moves an attempt straight from `STARTED` to
`CORRECT` or `INCORRECT`.)

```text
POST /challenge-attempts open attempt (STARTED)
POST /challenge-attempts/submit grade answer -> CORRECT | INCORRECT
PATCH /challenge-attempts/hint increments hintsUsed (max enforced upstream)
PATCH /challenge-attempts/reveal solution shown -> INCORRECT, score zeroed
PATCH /challenge-attempts/:id/expire -> EXPIRED
```

Submission runs in a TypeORM transaction holding a `pessimistic_write` lock on
the attempt row and is idempotent: replaying a graded submission returns the
cached result (`isDuplicateReplay`) instead of re-awarding anything. Answers
are graded by case-insensitive trimmed comparison against
`puzzle.correctAnswer`. Correct answers earn
`round(basePoints * (1 + bonus))` where the speed bonus is
`((timeLimit - timeSpent) / timeLimit) * 0.5` — up to +50%. Revealing the
solution forfeits the score immediately.

On a correct answer, in the same transaction: `XpLevelService.addXp(score)`
(XP equals score, 1:1), a `UserProgress` row is written, and the next
same-difficulty challenge is selected pseudo-randomly, excluding already
attempted puzzles. When the session's `challengeCount` target is reached
(default 5), the attempt is flagged `sessionCompleted`.

---

## 7. Progress tracking

Every graded submission appends one `UserProgress` row (user, puzzle/category,
optional daily quest link, correctness, answer, points earned, time spent).
The primary write path is inside the attempt transaction described above — a
direct service call, not event-driven, today. Reads are served by
`GET /progress` (paginated history), `GET /progress/stats` (totals, accuracy,
points, time), and `GET /progress/category/:id` (per-category breakdown).

A secondary pipeline in `progress-calculation.provider.ts` adds streak
multipliers (+10% at a 3-day streak, +25% at 7 days) and daily-quest completion
bonuses on its own path.

---

## 8. Reward architecture

Rewards are currently **eligibility-only**. `RewardService.checkEligibility({score, xp, correct})`
returns `{eligible, reason}`: ineligible if the answer was wrong or the score
is below `MINIMUM_SCORE_FOR_REWARD` (100); eligible otherwise. The verdict is
persisted on the completed session as `rewardEligible` / `rewardReason`.

No minting, transfer, reward record persistence, or chain call exists yet —
the richer canonical reward types (TOKEN, NFT, BADGE) in `src/domain` are
aspirational. Wiring eligibility results to on-chain redemption is the next
step (see [Blockchain integration](#9-blockchain-integration)).

---

## 9. Blockchain integration

Two distinct concerns, deliberately kept apart:

### Wallet linking and wallet authentication (off-chain, shipped)

- `users.stellarWallet` associates a wallet address with an account; it is set
during wallet login (see [Authentication](#4-authentication-architecture)).
- `POST /blockchain/wallet/link` validates the address string and emits
`wallet_linked` / `wallet_link_failed` events, consumed by the analytics
listener into `analytics_events`. It performs no chain interaction.
- Login signature verification is pure ed25519 over a server-issued nonce —
the smart contract is not involved.

### Soroban contract (on-chain, partially wired)

`contract/` is a Soroban contract built with `soroban-sdk` 23 and deployed to
the Stellar testnet; it backs on-chain rewards for challenge completion. The
release profile is tuned for wasm size (`opt-level = "z"`, LTO, symbols
stripped, `panic = "abort"`), and `ed25519-dalek` is pinned to `=2.2.0` for
reasons explained in its `Cargo.toml`.

The backend carries the configuration (`STELLAR_SECRET_KEY`,
`STELLAR_CONTRACT_ID`, `STELLAR_RPC_URL` defaulting to the public testnet RPC,
and `STELLAR_NETWORK_PASSPHRASE`) and `stellar-sdk`, but the dedicated
providers — register-player, submit-puzzle, get-player, sync-xp-milestone —
are still pending. Until they land, nothing in the runtime talks to the
contract.

---

## 10. AI integration (planned)

The target architecture reserves an AI Services slot beside the backend. There
is **no AI code in the repository today** — no LLM SDKs, no generation or hint
services. Planned roles are puzzle generation, difficulty tuning, and hint
assistance. When implementation starts it should live behind its own NestJS
module with the usual provider-per-use-case layering, and this section should
be updated to describe what actually shipped.

---

## 11. Frontend

- **Next.js 16 App Router** with React 19; routes are directories under `frontend/app/`.
- **State**: Redux Toolkit with `react-redux` for app state, TanStack Query for server state.
Expand All @@ -175,21 +344,7 @@ submits intent rather than computing outcomes.

---

## 5. Smart contract

`contract/` is a Soroban contract built with `soroban-sdk` 23 and deployed to
the Stellar testnet. It backs on-chain rewards for challenge completion.

The release profile is tuned for wasm size (`opt-level = "z"`, LTO, symbols
stripped, `panic = "abort"`). `ed25519-dalek` is pinned to `=2.2.0`; the comment
in `Cargo.toml` explains why, and that pin should not be bumped casually.

Wallet-based authentication does not require the contract: it verifies an
ed25519 signature over a server-issued nonce, entirely off-chain.

---

## 6. Data stores
## 12. Data stores

### PostgreSQL

Expand All @@ -210,7 +365,61 @@ failures show up immediately rather than at first cache read.

---

## 7. Deployment
## 13. External services

| Service | Used for | Configuration |
| ------- | -------- | ------------- |
| PostgreSQL ≥ 14 | System of record | `DATABASE_URL` or discrete `DATABASE_*` variables |
| Redis ≥ 6 | Token/session cache, GeoIP result cache | `REDIS_URL` (required at boot) |
| Google Identity | Verifying OAuth ID tokens at sign-in | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_CALLBACK_URL` |
| SMTP relay | Password-reset email delivery | `MAIL_HOST`, `MAIL_PORT`, `MAIL_SECURE`, `MAIL_USER`, `MAIL_PASSWORD`, `MAIL_FROM_NAME`, `MAIL_FROM_ADDRESS` |
| Stellar public RPC (testnet) | Soroban deployment/RPC target | `STELLAR_RPC_URL`, `STELLAR_NETWORK_PASSPHRASE`, `STELLAR_CONTRACT_ID`, `STELLAR_SECRET_KEY` |
| geoip-lite (bundled MaxMind DB) | Offline IP-to-country lookup for geolocation middleware | None — ships as a local dataset |

Hosting (Render for the backend, Vercel for the frontend) is covered under
[Deployment](#16-deployment).

---

## 14. Development environments

Prerequisites: Node.js 20.x LTS, npm 10 (workspaces), PostgreSQL ≥ 14,
Redis ≥ 6. Rust with the `wasm32-unknown-unknown` target and the Stellar CLI
are only required for `contract/` work. Docker is typically used as one-off
containers (`postgres:16` on 5432, `redis:7` on 6379) rather than a compose
stack.

- **Backend**: `cd backend && npm run start:dev` — serves on port 3000 with
Swagger at `/api` and health at `/health`. With `DATABASE_SYNC=true`,
TypeORM creates the schema on first boot, which is the normal local setup.
- **Frontend**: `cd frontend && npm run dev` (Next.js + Turbopack; falls back
to port 3001 when 3000 is taken). Point `NEXT_PUBLIC_API_URL` at the backend
via `frontend/.env.local`. Root-level `npm run dev:backend` /
`npm run dev:frontend` wrap the two.
- **Contract**: build/test with cargo from `contract/`; deploy to testnet with
the Stellar CLI.

Migration workflows and their current rough edges are documented in
[DEVELOPMENT.md §9](./DEVELOPMENT.md#9-migrations); environment variables are
catalogued in [ENVIRONMENT.md](./ENVIRONMENT.md).

---

## 15. Communication between services

- **Frontend ⇄ Backend**: synchronous HTTPS + JSON REST with Bearer JWTs. No
websockets or SSE anywhere yet.
- **Backend ⇄ PostgreSQL**: exclusively through TypeORM repositories.
- **Backend ⇄ Redis**: a single shared ioredis client (`REDIS_CLIENT`).
- **Inside the backend**: `EventEmitter2` domain events decouple side effects
(progress, streaks, analytics listeners) from request handlers;
`@nestjs/schedule` cron jobs perform nightly analytics rollups.
- **Backend ⇄ Chain**: planned Soroban provider calls over the Stellar RPC
(see [Blockchain integration](#9-blockchain-integration)).

---

## 16. Deployment

| Component | Host | Notes |
| --------- | ---- | ----- |
Expand All @@ -223,7 +432,7 @@ traffic drains before the process exits.

---

## 8. Conventions that shape the code
## 17. Conventions that shape the code

1. **Relative imports only.** Absolute `src/...` imports are rejected by CI.
2. **DTOs are the contract.** `forbidNonWhitelisted` means an undeclared field is a `400`, so every accepted field must exist on a DTO.
Expand All @@ -234,7 +443,29 @@ traffic drains before the process exits.

---

## 9. Where to go next
## 18. Where does a new feature belong?

Use this decision list to place new work:

- **Pure UI/UX change** (layout, styling, client-only interactions) → a route
under `frontend/app/` or a component in `frontend/components/`.
- **New player-facing capability with data** → a new NestJS module under
`backend/src/<feature>/` following the module / controller / providers /
entities / dtos layering, registered in `app.module.ts`.
- **Cross-cutting plumbing** (filters, middleware, pagination helpers) →
`backend/src/common/`.
- **Scoring or grading rule changes** → the challenge-attempt validation and
score helpers on the backend — never the client.
- **Reactions to things that already happen** (email, analytics, notifications)
→ an event listener subscribed to the relevant domain event, not inline
calls in the triggering handler.
- **Smart-contract logic** → the `contract/` Rust crate plus a backend
provider to invoke it; document any new variables in ENVIRONMENT.md.
- **Documentation** → `docs/`, alongside this file.

---

## 19. Where to go next

| Question | Document |
| -------- | -------- |
Expand Down
Loading