Case study · Gamified study platform · full-stack
StudyRaid
Gamified study platform: homework becomes quests with XP, levels, streaks, focus sessions and team challenges.
Every reward is computed on the server from an append-only ledger, and party challenges update live over WebSockets.
- Type
- Personal open-source project
- Automated tests
- 115 (pytest)
- Built with
- Python · FastAPI · Next.js
- Live demo
- Runs locally — not hosted

Overview
What it does
StudyRaid turns schoolwork into quests. Each quest has a subject, a difficulty and an estimate, and completing it pays XP and coins that level you up. A daily streak rewards consistency, a full-screen focus mode times deep work, 18 achievements unlock from what you actually do, and parties of up to eight friends take on shared weekly challenges. Analytics show XP and focus time by day, completion rate, best weekday and a 26-week heatmap.
Problem
Gamification only motivates if the numbers can be trusted. If the client can claim XP, if a double-click pays twice, if a streak breaks because a student in Kyiv finished at 00:30, or if a party counter drifts from what members did, the game stops meaning anything.
Solution
All rewards are computed on the server from an append-only ledger with a unique key per source, so every payout is idempotent. Streaks, party progress and analytics are recomputed from recorded activity in each user's own calendar. Live updates are published only after the transaction commits.
Screenshots
From the running application

Dashboard: level 17 with XP progress, a 12-day streak, today's quests and weekly XP 
Quest board with planned, in-progress and recently closed quests 
Full-screen focus session counting down a 50-minute block 
Analytics: XP by day, best weekday, subjects, outcomes and an activity heatmap 
Party page with a live weekly challenge, member contributions and a leaderboard 
Achievements unlocked from real activity, with progress toward the rest
Captured from the app running locally with its seeded demo data. Select an image to open it full size.
Architecture
How the pieces fit
- Next.js app → FastAPI routes
- FastAPI routes → Services
- Scheduler → Services
- Services → Rule engines
- Services → XP ledger & data
- Services → Realtime hub
- Realtime hub → WebSocket
- Core service
- External system
- Storage / entry point
Core features
What's in it
Quests with real weight
Difficulty and estimated effort set the base XP; early finishes, streaks and a daily soft cap adjust it on completion.
Level curve
Each level costs 100 XP more than the last. The closed form inverts exactly, so level-from-XP is O(1).
Streaks & freezes
One meaningful activity a day, in the student's time zone. Coins buy a freeze the scheduler spends on a missed day.
Focus mode
25, 50, 90 or custom minutes. The server decides when the time has passed; credit is capped at the plan.
Achievements
18 badges such as Night Owl, Boss Slayer and No Days Off, each a threshold on stats aggregated from real activity.
Parties & challenges
Invites by username or code, weekly goals, per-member contribution, and a live activity feed.
Scoped leaderboards
Only among people you share a party with, never global, and anyone can opt out.
Notifications
Due tomorrow, level-ups, challenge results and a streak warning, pushed live and deduplicated.
Technical decisions
Interesting problems, and how they're solved
- 01
The ledger is the source of truth
Every XP and coin change is a row keyed by its source (quest:42, achievement:first_blood). Totals are a cache updated in the same transaction, and a test holds them equal to the ledger.
- 02
Progress is derived, not counted
Challenge progress is recomputed from members' activity inside the window, counted from the moment each member joined. Settlement is a conditional update, so a race can't pay twice.
- 03
Publish after commit
Services queue realtime events on the session; they reach WebSockets only once the transaction commits. Nobody sees XP that was rolled back.
- 04
The clock is a parameter
Routes read the time once and pass it down. Tests are deterministic, and the demo replays 100 days of history through production code.
async def award(
session: AsyncSession,
user: User,
*,
source: LedgerSource,
ref: str,
xp: int,
coins: int,
now: datetime,
counts_for_streak: bool = False,
detail: dict[str, Any] | None = None,
) -> LedgerEntry | None:
"""Append a ledger entry and update the cached totals. Idempotent per `ref`."""
if await ledger_repo.exists(session, user.id, ref):
return None
entry = LedgerEntry(
user_id=user.id,
source=source,
source_ref=ref,
xp=xp,
coins=coins,
counts_for_streak=counts_for_streak,
local_date=local_date(now, user.timezone),
created_at=now,
detail=detail or {},
)
try:
async with session.begin_nested():
session.add(entry)
except IntegrityError: # lost a race with an identical request
return None
level_before = level_for_xp(user.total_xp)
user.total_xp += xp
user.coins += coins
level_after = level_for_xp(user.total_xp)Security
What protects the data
Rotating refresh tokens
Opaque, stored as SHA-256 digests in an HttpOnly cookie and rotated on each use. Replaying an old token revokes the whole login.
Server-side rewards
Request bodies reject unknown fields, so a client can't send XP. Rewards come from facts the server owns.
404 for foreign data
Ownership and membership are part of every lookup, so other users' quests and parties look like they don't exist.
WebSocket tickets
Sockets authenticate with a single-use 30-second ticket and a checked Origin, never a long-lived token in the URL.
Passwords & limits
argon2id, identical timing for unknown emails, and rate limits on login, registration and writes.
CSRF
Cookie endpoints require a custom header that cross-site pages can't send without a CORS preflight.
Testing
How it's verified
115 pytest tests drive the real app over HTTP and WebSockets with a controllable clock: auth and token reuse, cross-user access, reward rules, level boundaries, time-zone streak days, achievements, focus timing, challenge settlement, idempotent scheduler runs and live event delivery. CI runs the suite on SQLite and PostgreSQL and checks that the Alembic migrations match the models exactly.
Stack & skills
Built with
Technology
- Python
- FastAPI
- Next.js
- WebSockets
- SQLAlchemy 2
- PostgreSQL
- TypeScript
- Tailwind CSS
- Recharts
- Docker
What this project demonstrates
- Full-stack product design
- Game-system design
- Realtime (WebSockets)
- Session security
- Time-zone-correct logic
- Data modelling
- PostgreSQL + SQLite
Repository & demo
Run it yourself
Seeds six fictional students with about 100 days of history, generated through the real services. Or run docker compose up to use PostgreSQL.
No public live demo is hosted. Everything below runs locally with Python 3.12+ and Node 20+.
$ git clone https://github.com/Moeijiro/studyraid.git
$ cd studyraid
$ make install
$ make seed # ~100 days of demo history
$ make api # FastAPI on :8000
$ make web # Next.js on :3000
Have a project in mind?
I build APIs, automation systems, Discord integrations and full-stack applications. Tell me what it should do and I'll tell you how I'd build it.
