Skip to content

Repository files navigation

Studiem

Studiem

Real-time 1v1 AP exam battles. Two students face the same questions at the same time — faster and more accurate wins. Per-subject ELO, streaks, and a validated question bank.

CI Next.js React TypeScript Node Socket.io Supabase License


Status

Studiem is deployed and live at studiem.app. Accounts, practice mode, per-subject ELO, the leaderboard, friends, chat, and direct challenges are all working in production.

Matchmaking is the one exception — it is built and functional, but still a prototype. The queue pairs players by ELO bracket and runs a full battle end to end, so you can play a real match through it today. What it is not yet is production-hardened: it runs on a single instance with in-memory state, and pairing behaviour under low queue volume or concurrent joins is still being tuned. If you are evaluating this repo and matchmaking behaves unexpectedly, that is known and actively being worked on — everything around it is stable.

Area State
Auth (email/password + Google OAuth) ✅ Live
Practice mode & card mastery ✅ Live
Per-subject ELO & leaderboard ✅ Live
Friends, presence, chat, challenges ✅ Live
Question bank (~1,000 AP Chemistry cards) ✅ Live
1v1 matchmaking 🚧 Prototype — works, not yet hardened

What it is

Studying alone is boring and low-retention. Quizlet's multiplayer is asynchronous, Kahoot needs a teacher to host, and ChatGPT has no matchmaking or ranking. Studiem is self-serve, real-time, and ranked.

  • 1v1 battles — matchmaking pairs you with an opponent in your ELO bracket, then both players get the identical question set (prototype — see Status)
  • Per-subject ELO — your AP Chemistry rating is independent of your AP Biology rating
  • Practice mode — solo drills with per-card mastery tracking, retry-missed, and endless queues
  • Social layer — friends, presence, in-battle chat, and direct challenges
  • Validated content — ~1,000 AP Chemistry cards across Units 1–9, each answered blind by a validator model before it can be served

Architecture

┌──────────────────────────────────────────────────────────┐
│  Browser — Next.js 16 / React 19 (Vercel)                │
│  app/  components/  lib/socket.ts  middleware.ts         │
└─────────┬───────────────────────────┬────────────────────┘
          │ Socket.io (WS / polling)  │ Supabase JS (REST + auth)
          ▼                           ▼
┌─────────────────────┐  ┌────────────────────────────────┐
│  Battle Server      │  │  Supabase (Postgres + Auth)    │
│  Node 18+ · Express │◄─│  profiles      elo_ratings     │
│  Socket.io (Railway)│  │  battles       source_cards    │
│                     │─►│  question_variants friendships │
│  index.js  elo.js   │  │  user_card_stats  leaderboard  │
│  questions.js       │  │  Email/password + Google OAuth │
└─────────────────────┘  └────────────────────────────────┘

Two independent services. They never call each other over HTTP — all real-time traffic is Socket.io, and both talk to Supabase directly with different keys (anon key in the browser, service role key server-side only).

Full detail lives in ARCHITECTURE.md.

Notable design decisions

Decision Why
Railway (not Vercel) for the server Socket.io needs a persistent process; serverless cannot hold connections
Server-side question selection Clients can never pre-load answers; the reviewed gate is enforced server-side
In-memory battle state Zero-latency game events. Trades away horizontal scaling — see below
Service role key server-only The browser has no write access to ELO or battle records
Async per-player battle flow Each player answers at their own pace; no lockstep waiting between questions

Known scaling limit: battles, queue, and presence are process-local maps, so the battle server runs as a single instance. Horizontal scaling requires a Redis adapter for Socket.io first.


Tech stack

Layer Choice
Frontend Next.js 16 (App Router), React 19, TypeScript (strict), Tailwind CSS 4
Realtime Socket.io 4
Backend Node.js 18+, Express 5
Database & Auth Supabase (Postgres, RLS, Supabase Auth)
Hosting Vercel (web) · Railway (server)
Content pipeline Writer/validator LLM agents over a typed card schema

Getting started

Prerequisites

  • Node.js 18 or newer
  • A Supabase project (free tier is fine)

1. Clone and install

git clone https://github.com/seanluofficial/studiem.git
cd studiem
npm install --prefix server
npm install --prefix web

2. Set up the database

Run every file in supabase/migrations/ in filename order in the Supabase SQL editor:

001_initial.sql → 002_questions.sql → 003_seed_apchem.sql → … → 013_user_card_stats.sql

Migrations are not auto-applied. Then import the question bank:

node scripts/import.js

3. Configure environment

Copy the examples and fill in your Supabase credentials:

cp server/.env.example server/.env
cp web/.env.example web/.env.local

server/.env

Variable Description
SUPABASE_URL Your project URL
SUPABASE_SERVICE_ROLE_KEY Service role key — server only, never expose to the browser
PORT Defaults to 4000 locally; Railway sets this in production

web/.env.local

Variable Description
NEXT_PUBLIC_SUPABASE_URL Same project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY Anon/public key
NEXT_PUBLIC_SOCKET_URL Battle server URL — http://localhost:4000 in dev

4. Run both services

# terminal 1 — battle server
npm run dev --prefix server

# terminal 2 — web app
npm run dev --prefix web

Open http://localhost:3000.


Project structure

web/                      Next.js frontend
  app/                    Routes: lobby, battle, practice, leaderboard, profile, auth
  components/             BattleRoom, PracticeMode, FriendsPanel, ChatBox, …
  lib/socket.ts           Socket.io client singleton
  lib/supabase/           Browser + server Supabase helpers
  middleware.ts           Auth guard on every route

server/
  index.js                Socket.io server — matchmaking, game loop, presence
  elo.js                  Per-subject ELO calculation and persistence
  questions.js            Question loader (Supabase, JSON fallback for local dev)
  streak.js  stats.js     Streaks and per-card mastery

supabase/migrations/      Ordered SQL migrations
scripts/                  Content generation, validation, and bulk import
content/                  Source question JSON + CED data per subject

ARCHITECTURE.md           System design and data flow
PRD.md                    Product spec
CARD_SCHEMA.md            Question card schema
DEVELOPMENT_ROADMAP.md    Prioritized work queue
TECH_DEBT.md              Known debt, tracked by ID

Development

Run these before committing — all must pass:

npm run typecheck --prefix web     # TypeScript, strict
node --check server/index.js       # Server syntax
node --check server/elo.js
node --check server/questions.js

CI runs the same checks on every push and pull request.

Conventions

  • TypeScript stays strict — no any, no @ts-ignore
  • All socket payloads are untrusted: validate ELO, subject, display name, and client timings server-side before use
  • Never authorize off a client-supplied roomId — verify the socket is actually in the battle
  • New migrations go in supabase/migrations/NNN_description.sql, incrementing from the last file

Security

The server is the only authority. Every value arriving from a socket event is validated — ELO is clamped and NaN/Infinity rejected, subjects are checked against a known set, display names are trimmed and length-capped, and client-reported timings are bounds-checked so a client cannot win every tiebreak by reporting 1ms.

Found a vulnerability? See SECURITY.md. Please do not open a public issue.


License

Proprietary — all rights reserved. See LICENSE. The source is public for reference and review; it is not licensed for reuse, redistribution, or commercial operation.

About

Real-time 1v1 AP exam battle app - two students face the same questions simultaneously, with per-subject ELO ranking. Next.js 16, Socket.io, Supabase.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages