A comprehensive SAT practice platform with three distinct modes: Study, Quiz, and Test. Built with Next.js and Node.js/Express, with SQLite for progress tracking and Google OAuth for authentication.
Docker image: technicallybob/dragonsat
💖 Like what you see? If DragonSAT helps you or your students, consider supporting its development — hit the Sponsor button on the right side of this repo!
- Study Mode — Learn at your own pace with immediate feedback and unlimited time
- Quiz Mode — Practice with a soft timer and end-of-session score summary
- Test Mode — Full SAT simulation with hard time limits and a post-test review screen
- User Accounts — Email/password registration with optional Google Sign-In
- Progress Tracking — All sessions and responses stored in SQLite
- History & Stats — Session history view with per-mode breakdowns and lifetime accuracy
- Domain Filtering — Study specific SAT question categories
- Difficulty Selection — Choose Easy, Medium, or Hard questions
- LaTeX Rendering — Full math expression support via KaTeX
- Dark Mode — Full light/dark theme support
- Settings — Configurable display name, sound effects, dark mode toggle, and data export
- Admin Console — User management for admins: list users, delete accounts, reset passwords, promote/demote roles. The first registered user is automatically granted the admin role
DragonSAT uses a dual-window design:
- Main window (
/) — Sidebar layout with Dashboard, History, and Settings - Session window (
/session?mode=...) — Opened viawindow.open()when a session starts; runs independently and communicates back to the main window viapostMessage
dragonsat/
├── frontend/ # Next.js 14 application
│ ├── app/
│ │ ├── page.tsx # Main window (SetupLayout)
│ │ ├── session/ # Session window
│ │ └── api/proxy/ # Server-side API proxy
│ ├── components/ # React components
│ ├── hooks/ # Zustand stores + custom hooks
│ ├── utils/ # API client, parser, scoring, sounds
│ └── styles/
├── backend/ # Node.js/Express API server
│ └── src/
│ ├── routes/ # /api/questions, /api/progress, /api/auth, /api/admin
│ ├── services/ # OpenSAT client, progress logic
│ ├── middleware/ # JWT auth, CORS
│ └── db/ # SQLite schema + init
├── docker-compose.yml # Single-container deployment
└── .env.example # Environment variable template
The easiest way to run DragonSAT.
cp .env.example .env# Required
JWT_SECRET=your-long-random-secret-here # openssl rand -hex 32
# Optional — Google Sign-In (set both to enable)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Optional overrides
FRONTEND_URL=http://localhost:3000docker compose up -dOpen http://localhost:3000.
Data persists in ./data/dragonsat.db via the mounted volume.
Google Sign-In is pre-configured in the published image. To enable it, set
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETin your.env— no rebuild required.
- Node.js 18+
- npm
cd backend
cp .env.example .env
# Edit .env — set JWT_SECRET and optionally Google OAuth vars
npm install
npm run devRuns on http://localhost:3001.
cd frontend
cp .env.example .env.local
# Edit .env.local — set NEXT_PUBLIC_API_URL and optionally NEXT_PUBLIC_GOOGLE_CLIENT_ID
npm install
npm run devRuns on http://localhost:3000.
| Variable | Description |
|---|---|
NEXT_PUBLIC_API_URL |
http://localhost:3001/api for local dev; /api/proxy in Docker |
BACKEND_URL |
Server-side proxy target (Docker only); not needed for local dev |
NEXT_PUBLIC_GOOGLE_CLIENT_ID |
Google OAuth client ID (optional) |
| Variable | Default | Description |
|---|---|---|
NODE_ENV |
development |
|
PORT |
3001 |
Backend listen port |
DATABASE_PATH |
./data/dragonsat.db |
SQLite file path |
OPENSAT_API_URL |
https://pinesat.com/api/questions |
Question source |
FRONTEND_URL |
http://localhost:3000 |
CORS allowed origin |
JWT_SECRET |
— | Required. Sign with openssl rand -hex 32 |
GOOGLE_CLIENT_ID |
— | Google OAuth (optional) |
GOOGLE_CLIENT_SECRET |
— | Google OAuth (optional) |
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/register |
Register with name, email, password |
POST |
/api/auth/login |
Login with email + password |
GET |
/api/auth/me |
Get current user (JWT required) |
PATCH |
/api/auth/profile |
Update display name (JWT required) |
POST |
/api/auth/google |
Sign in with Google access token |
POST |
/api/auth/link-google |
Link Google to existing account (JWT required) |
POST |
/api/auth/change-password |
Change password (JWT required) |
| Method | Path | Description |
|---|---|---|
GET |
/api/questions |
Get filtered questions (domain, difficulty, limit) |
GET |
/api/questions/:id |
Get a specific question |
GET |
/api/domains |
List available domains |
GET |
/api/sections |
List available sections |
GET |
/api/cache-status |
Check if OpenSAT data is loaded |
| Method | Path | Description |
|---|---|---|
POST |
/api/progress/user |
Create or get user record |
POST |
/api/progress/session/start |
Start a session |
POST |
/api/progress/session/end |
End session with score |
POST |
/api/progress/response |
Record a question response |
GET |
/api/progress/session/:id |
Get responses for a session |
GET |
/api/progress/user/:id |
Get user stats and history |
GET |
/api/progress/analytics |
Get per-domain accuracy breakdown (JWT required) |
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/users |
List all users |
DELETE |
/api/admin/users/:id |
Delete user and all their data |
POST |
/api/admin/users/:id/reset-password |
Set a new password for a user |
PATCH |
/api/admin/users/:id/role |
Change user role (admin or user) |
First user = admin: The very first account registered on a fresh database is automatically granted the
adminrole. Additional admins can be promoted from the Admin Console.
| Mode | Timer | Feedback | Navigation |
|---|---|---|---|
| Study | None | Immediate (Check Answer) | Free |
| Quiz | Soft cap | End of session | Linear (Next) |
| Test | Hard stop | Score report + review | Linear + review screen |
Frontend: Next.js 14, React 18, TypeScript, Zustand, Tailwind CSS, KaTeX, react-markdown
Backend: Express.js, SQLite (sqlite3), TypeScript, Axios, JWT (jsonwebtoken)
Backend won't start — Verify port 3001 is free. Check DATABASE_PATH directory is writable.
Frontend can't reach backend — Confirm NEXT_PUBLIC_API_URL matches where the backend is running. In Docker, this is /api/proxy (server-side proxy).
Questions not loading — Check /api/cache-status. Backend fetches the OpenSAT dataset on startup and caches it in memory. If it fails, check network access to pinesat.com.
Google Sign-In button not showing — NEXT_PUBLIC_GOOGLE_CLIENT_ID must be set at build time (it's baked into the Next.js bundle). Restart the dev server after adding it to .env.local.
Session window won't open — Some browsers block window.open() if not triggered directly from a user gesture. Ensure pop-ups are allowed for the app's origin.
MIT — see LICENSE.