A course scheduling tool for Western Washington University students. Add your courses, set preferences, and generate optimized, conflict-free schedules instantly.
Disclaimer: This project is not affiliated with Western Washington University. It is an independent initiative developed for educational and personal use. All data is for informational purposes only and should not be considered official or binding.
- Add courses by subject + number (e.g., "CSCI 247") or by CRN
- Mark courses as required or optional with configurable min/max bounds
- Pin specific sections to lock in a preferred CRN
- Blocked times — paint time slots as unavailable with named groups, custom colors, opacity, and hatching patterns
- Browse generated schedules with scoring based on gaps, start time, and end time preferences
- View async/TBD courses separately
- Final exams on each section with a scheduled final, matched to the registrar's finals schedule; times the meeting pattern leaves ambiguous (labs, several meeting times, once-a-week classes) are marked as guesses
- Finals week view of the current schedule's exams, toggled from the schedule header
- Add to Calendar: download an
.icsfile with classes (holidays skipped), final exams in their class's room, holidays, registration phases, and add/drop/withdraw deadlines, the term's dates titled short (W27 - Drop deadline (no W)) with the registrar's full wording in the description
- Sidebar status line for the selected term: countdown to registration, the next registration phase, or finals week, and a pointer to the next term when its registration is about to open
- Key dates popover: registration phases, classes, holidays, and finals week, marked when the dates are still estimates
Under the hood, schedule generation uses a bitmask-based conflict detection algorithm with backtracking and pruning. Each section is encoded as a 450-bit time mask (5 days x 90 ten-minute slots from 7am-10pm), enabling O(1) conflict checks. On a modern CPU, conflict detection runs in ~3ns and a full 10-course generation completes in under 2ms.
- Search across a single term, an academic year, or all historical data
- Filter by subject, course number, title, instructor, open seats, and credit range
- Results scored by relevance and term recency
- Add courses or specific CRNs directly from search results to the schedule builder
- Automated scraping from WWU's Banner API with concurrent page fetches
- Background job scheduler that scrapes each term on a cadence set by its phase (every 8 hours during registration, daily before registration and in session, once when past; a failed scrape retries hourly), with phases taken from the registrar's published registration and term dates
- Course-centric data model supporting cross-term search — organized by unique course identity, not term hierarchy
- SQLite with WAL mode for concurrent reads during scraping
- Dark/light theme with system preference detection
- Mobile-responsive layout with drawer navigation
- Export schedules as PNG (with native share API on mobile)
- Announcement banners and in-app feedback submission
- All state persisted to localStorage (courses, filters, schedules, preferences)
| Layer | Technology |
|---|---|
| Backend | Go 1.26, Gin, SQLite (WAL mode) |
| SQL | sqlc (type-safe codegen), golang-migrate |
| Frontend | React 19, TypeScript, Vite |
| Styling | Tailwind CSS v4, shadcn/ui |
| State | Zustand (persisted), TanStack Query |
| Testing | Go test + benchmarks, Vitest |
| Logging | slog + tint |
Banner API → Scraper ─┐
registrar.wwu.edu → Calendar ─┴→ SQLite → Cache (in-memory) → REST API → React Frontend
↑
Jobs Service (scheduled scraping)
backend/
├── cmd/server/ # Entry point
├── migrations/ # SQL migration files (golang-migrate)
├── internal/
│ ├── api/ # HTTP handlers
│ ├── cache/ # In-memory term snapshots, loaded on request, dropped after jobs rewrite them
│ ├── calendar/ # Registrar calendar scraper, term phases, finals matching
│ ├── config/ # Environment configuration
│ ├── db/ # SQLite connection setup
│ ├── generator/ # Bitmask conflict detection + backtracking
│ ├── jobs/ # Background job scheduler (scrape scheduling)
│ ├── meeting/ # Meeting-time payload shared by every section response
│ ├── scraper/ # Banner API client + data extraction
│ ├── search/ # Course search with scoring
│ ├── server/ # Router, middleware, graceful shutdown
│ ├── static/ # Embedded frontend serving
│ ├── store/ # sqlc-generated query code
│ └── termcode/ # Term code (YYYYQQ) helpers
├── Makefile
└── sqlc.yaml
frontend/src/
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── schedule/ # Grid, time blocks, course dialogs, blocked times
│ └── schedule-builder/ # Course input, rows, previews
├── hooks/ # API hooks, theme, export, drag-to-paint
├── stores/ # Zustand store (persisted to localStorage)
└── lib/ # API client, schedule utils, term status, finals, .ics export
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/health |
Health check |
GET |
/api/terms |
Available terms with phase, start and end, and registration opening |
GET |
/api/calendar/:term |
Registration phases, holidays, deadlines, and finals week |
GET |
/api/subjects |
Subject codes (optionally by term) |
GET |
/api/course/:subject/:courseNumber |
Course details + sections |
GET |
/api/search |
Filtered course search |
GET |
/api/crn/:crn |
CRN lookup |
POST |
/api/courses/validate |
Batch validate courses |
POST |
/api/generate |
Generate schedule combinations |
GET |
/api/announcement |
Active announcement |
POST |
/api/feedback |
Submit feedback |
Section payloads from generate, search, and CRN lookup carry final ({date, start, end, certain, reason?, location?}, the location being the room of the meeting time the exam is based on) when the section has a final exam slot, and mark lab meetings with lab: true. Calendar deadlines carry the registrar's wording as name and a short title for calendar events.
- Go 1.26+
- Node.js 22.13+ and pnpm
- SQLite 3
- sqlc —
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest - golang-migrate —
go install -tags 'sqlite3' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
cd backend
cp .env.example .env # configure environment
make migrate-up # apply database migrations
make run # start server (default port 48920)cd frontend
pnpm install
pnpm dev # dev server at http://localhost:5173cd frontend && pnpm build # compile frontend
cd ../backend && make build # builds binary with embedded frontend
./bin/server # single binary serves everything# Backend
make test # run all Go tests
make bench # run benchmarks
make sqlc # regenerate query code after editing queries.sql
# Frontend
pnpm lint # eslint
pnpm test # vitest
pnpm format # prettierPORT=48920 # server port
ENVIRONMENT=development # development | production
DATABASE_PATH=data/schedule.db # SQLite database location
CORS_ALLOWED_ORIGINS=http://localhost:5173
SCRAPER_CONCURRENCY=4 # parallel page fetches
JOBS_ENABLED=true # background scraping
JOBS_ACTIVE_SCRAPE_HOURS=8 # hours between scrapes of terms in registration
JOBS_DAILY_SCRAPE_HOUR=3 # Pacific hour (0-23) for daily scrapes
JOBS_PAST_TERM_YEARS=5 # years of historical data to backfillThe frontend build reads VITE_SITE_URL (the production origin, used by index.html's canonical and social tags and as the host in calendar event UIDs) from frontend/.env. Override it in frontend/.env.local or the build environment; the build fails if it is unset.
The project uses table-driven tests throughout. Backend test coverage spans all major packages:
internal/generator — unit + benchmark (bitmask, backtracking, scoring)
internal/search — unit + fuzz + benchmark
internal/jobs — unit (scheduler, simulated scrape cadence)
internal/calendar — unit + integration (parsers on captured pages, phases, storage, finals matching, term calendar)
internal/api — integration (handler endpoints)
internal/cache — unit
internal/scraper — unit
internal/db — unit
internal/config — unit
internal/static — unit
Frontend tests cover the Zustand store, schedule utilities, and warning generation (118 tests).
Cooper Morgan — Author & Maintainer cwooper.dev · @Cwooper
Konnor Kooi — Previous Contributor konnorkooi.com · @konnorkooi
Previous contributors include Rory Bates, Arne Wiseman, and Ben Huynh.
MIT License — see LICENSE for details.