GRASP is a web application that helps UBC instructors turn course materials into evidence-based formative assessments. Instructors upload lecture content, generate and review AI-authored questions, and publish quizzes; students get spaced, adaptive, and elaborative practice — all behind UBC Single Sign-On.
- AI question generation — Upload lecture material (text, PDF, DOCX, URLs) and generate evidence-based questions grounded in your content via a retrieval-augmented (RAG) pipeline.
- Multiple question types — Multiple choice, fill-in-the-blank, calculation, and open-ended questions, with support for math (KaTeX) and chemistry (SMILES) rendering.
- Question review & banking — Review, edit, flag, and organize generated questions in a per-course question bank before they reach students.
- Quizzes & scoring — Build quizzes from the bank, publish them to students, and track scores and quiz summaries.
- Student practice experience — A dedicated student dashboard with quizzes, achievements, and adaptive practice.
- Course materials & onboarding — Manage uploaded materials per course and guide new instructors through setup.
- User management — Faculty can view and manage course members.
- UBC SSO — SAML-based Single Sign-On / Single Log-Out, with role-based access (faculty / staff / student).
- Frontend: React 18, Vite, Tailwind CSS, React Router, TanStack Query, Zustand
- Backend: Node.js, Express 5
- Database: MongoDB
- AI / RAG: UBC GenAI Toolkit (LLM, embeddings, chunking, RAG), Qdrant vector store, OpenAI or Ollama as the LLM provider
- Auth: Passport + SAML (UBC Shibboleth)
- Testing: Cypress (end-to-end)
tlef-grasp/
├── client/ # React frontend (Vite + Tailwind)
│ ├── src/ # pages, components, hooks, stores
│ ├── dist/ # built assets, served in production (committed)
│ └── cypress/ # end-to-end tests
├── src/ # Express backend
│ ├── server.js # server entry (API + serves client/dist)
│ ├── routes/ # API + auth route definitions
│ ├── controllers/ # request handlers
│ ├── services/ # business logic, data access, RAG
│ ├── models/ # question models
│ └── middleware/ # auth, session, passport, database
├── .env.example # environment variable template
├── package.json # backend dependencies and scripts
└── README.md # this file
- Node.js 18+
- npm
- A running MongoDB instance
- A running Qdrant instance (for the question-generation vector store)
- An LLM provider — an OpenAI API key, or a local Ollama server
- For login: a SAML Identity Provider. For local development this is typically the
docker-simple-samlproject.
Install backend and frontend dependencies:
npm install
npm --prefix client installCopy the template and fill in your values:
cp .env.example .envSet CANVAS_DOMAIN, CANVAS_CLIENT_ID, CANVAS_CLIENT_SECRET, and
CANVAS_REDIRECT_URI (see .env.example) to enable the Canvas controls in
instructor Settings and My Sections. If any of them is absent, Canvas is hidden
and every section keeps syncing students from the UBC Academic API.
Each instructor connects their own Canvas account (OAuth, per GRASP user) and
links a GRASP section to one Canvas section. GRASP calls these Canvas API
endpoints with that instructor's token, and never relies on include[]
parameters (a scoped developer key strips them):
GET /api/v1/courses— the courses the instructor teaches (enrollment_type=teacher)GET /api/v1/courses/:course_id/sections— sections offered for linkingGET /api/v1/courses/:course_id/enrollments(type[]=StudentEnrollment,state[]=active,invited) — the roster read by Sync from Canvas; Canvas embeds each enrollment's user, withintegration_idwhen the token may read SIS data
A developer key with Enforce Scopes on refuses an OAuth request that names
no scopes (or any scope the key lacks), and answers any API call outside the
token's scopes with 401. Set CANVAS_SCOPES to the scopes GRASP should request
(whitespace- or comma-separated, each one of the key's scopes):
- Unset/empty — GRASP requests no scopes and every Canvas feature is on. This only works on a key without Enforce Scopes (e.g. local dev Canvas).
- Set — GRASP requests exactly that list, and each Canvas feature is on only
when all of its scopes are listed (
src/lms/canvas-scopes.jsholds the map;/api/lms/canvas/statusreports the result ascapabilities). A disabled feature is hidden in the UI and its routes answer 409capability-disabled. A Canvas-linked section whose deployment lacks the roster-sync scopes shows "Canvas roster sync isn't enabled on this deployment" and never falls back to the Academic API.
Recommended value for UBC's current production key (enables linking and roster sync):
CANVAS_SCOPES="url:GET|/api/v1/courses url:GET|/api/v1/courses/:course_id/sections url:GET|/api/v1/courses/:course_id/enrollments url:GET|/api/v1/courses/:course_id/assignments"
| GRASP feature | Scopes it needs |
|---|---|
Link a section (link) |
url:GET|/api/v1/courses, url:GET|/api/v1/courses/:course_id/sections |
Sync from Canvas (rosterSync) |
url:GET|/api/v1/courses, url:GET|/api/v1/courses/:course_id/enrollments |
Canvas assignments (assignments, issue #113 item 4, not built yet) |
url:GET|/api/v1/courses/:course_id/assignments plus the four below |
To enable Canvas assignments, add these four scopes to the developer key and
then to CANVAS_SCOPES:
url:POST|/api/v1/courses/:course_id/assignments
url:GET|/api/v1/courses/:course_id/assignments/:assignment_id/overrides
url:PUT|/api/v1/courses/:course_id/assignments/:assignment_id/overrides/:id
url:POST|/api/v1/courses/:course_id/assignments/:assignment_id/overrides
Changing CANVAS_SCOPES requires instructors to reconnect Canvas: an
existing token keeps the scopes it was granted. Canvas answers both an expired
token and a call outside the token's scopes with 401, so GRASP reports either as
"Canvas rejected the request. Reconnect Canvas; if this keeps happening, the
GRASP Canvas developer key may be missing a permission."
Sync from Canvas (My Sections, per linked section) replaces the Academic API sync for Canvas-linked sections while Canvas is configured; unlinked and Moodle-linked sections keep the Academic API sync. It works like this:
- It reads the Canvas students (
StudentEnrollment, active or invited) of the linked Canvas section only — Test Student, TAs, teachers, and observers are excluded — after re-checking that the instructor still teaches the linked Canvas course. - Students are matched to GRASP accounts by Canvas
integration_id, which at UBC is the PUID, and by nothing else. Students who have never signed in get a placeholder account keyed by their PUID. Rows without anintegration_idare reported as unmatched, never guessed. Canvas only returnsintegration_idto tokens allowed to read SIS data, so the instructor's Canvas role needs that permission: with nointegration_idat all the sync refuses and changes nothing, and below 80% coverage it adds who it can but drops no one. - Students no longer on the Canvas section are soft-dropped from the GRASP section (they lose quiz access for it; a later sync restores them if they come back). The first sync after linking or re-linking a section never drops anyone without the instructor confirming the list, and neither does a sync that would drop more than half the section.
- Adds, restores, and drops are recorded in the course's access history with the syncing instructor as the actor.
Set MOODLE_DOMAIN to enable the Moodle controls in instructor Settings (for
the local Moodle container, use http://localhost:9200). If it is absent, the
Moodle tab and link buttons are hidden.
The Moodle site must have REST web services enabled and expose these functions to the service used by GRASP:
core_webservice_get_site_infocore_enrol_get_users_coursescore_group_get_course_groups
Each instructor generates their own Moodle web-service token and pastes it into GRASP. The token is stored per GRASP user; it is not shared with co-instructors.
npm run devThis runs the Express backend (port 8070) and the Vite dev server (port 5173)
in parallel. The Vite dev server proxies /api, /auth, and /Shibboleth.sso
requests to the backend, so you only need to open the frontend.
Visit http://localhost:5173/ in your browser. You'll be taken through SSO and into the dashboard.
Backend (run from the project root):
npm run dev— Start backend + frontend together (development).npm run dev:server— Start only the Express backend (with nodemon).npm run dev:client— Start only the Vite frontend.npm run build— Build the React client intoclient/dist.npm start— Run the production server (serves the built client fromclient/distat port8070).
Frontend (run from client/):
npm run lint/npm run format— Lint / format the client source.npm run cypress:open/npm run test:e2e— Run end-to-end tests. See client/cypress/README.md for setup.
This project is owned by the Learning Technology Innovation Centre (LTIC) at the University of British Columbia. All rights reserved.