Skip to content
Moeijiro
← All projects

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.

Full-Stack Apps
Type
Personal open-source project
Automated tests
115 (pytest)
Built with
Python · FastAPI · Next.js
Live demo
Runs locally — not hosted
Dashboard: level 17 with XP progress, a 12-day streak, today's quests and weekly XP

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
    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
    Quest board with planned, in-progress and recently closed quests
  • Full-screen focus session counting down a 50-minute block
    Full-screen focus session counting down a 50-minute block
  • Analytics: XP by day, best weekday, subjects, outcomes and an activity heatmap
    Analytics: XP by day, best weekday, subjects, outcomes and an activity heatmap
  • Party page with a live weekly challenge, member contributions and a leaderboard
    Party page with a live weekly challenge, member contributions and a leaderboard
  • Achievements unlocked from real activity, with progress toward the rest
    Achievements unlocked from real activity, with progress toward the rest
  • Dashboard on a phone
    Dashboard on a phone

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 appReact · Motion · RechartsWebSocketsingle-use ticketFastAPI routesauth · quests · partiesSchedulerreminders · expiryServicesquests · focus · challengesRule engineslevels · rewards · streaksXP ledger & dataSQLite · PostgreSQLRealtime hubpost-commit outbox
  • 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

backend/app/services/progression.py · L29–64View on GitHub ↗
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.