Adaptive learning platform. Multi-tenant white-label SaaS.
- CLI or MCP for everything. Browser auth is only used to mint credentials for the CLI; course creation and publishing stay programmatic.
- Use
bunnotnpm. - Read
docs/adding-a-course.mdbefore building any course. It is the canonical agent runbook.
If a user asks you to build, draft, author, or create a course, follow this section. Do NOT improvise a workflow — use the CLI/MCP tools below.
You can scaffold, fill, validate, and review courses without any account. These operations run locally.
You MUST authenticate before importing or publishing. Four tools require auth: graspful_import_course, graspful_publish_course, graspful_import_brand, graspful_list_courses.
To authenticate, do ONE of:
- CLI (recommended for first-time setup): Run
graspful register [--email <email>]— this opens browser auth, then saves an API key to~/.graspful/credentials.json. - Environment variable: Set
GRASPFUL_API_KEY=gsk_...before starting the MCP server.
If you try an authenticated tool without auth, you'll get a clear error telling you to register first.
npx @graspful/cli initThis auto-detects your editor (Claude Code, Cursor, Windsurf, Codex) and configures MCP if supported. No global install needed.
If you have MCP configured, skip the CLI and use MCP tools directly (see Step 1b).
If MCP is already configured, you have these tools available — no CLI needed:
| Tool | Auth? | Description |
|---|---|---|
graspful_create_academy |
No | Generate academy plan and manifest YAML |
graspful_scaffold_course |
No | Generate course YAML skeleton |
graspful_fill_concept |
No | Add KPs and problems to a concept |
graspful_validate |
No | Validate YAML against schema |
graspful_review_course |
No | Run 10 quality checks, including teaching alignment |
graspful_describe_course |
No | Course statistics |
graspful_create_brand |
No | Generate brand YAML (required — every org needs a brand) |
graspful_import_academy |
Yes | Import academy manifest + course YAMLs (set GRASPFUL_API_KEY first) |
graspful_import_course |
Yes | Import course to platform (set GRASPFUL_API_KEY first) |
graspful_publish_course |
Yes | Publish a draft course (set GRASPFUL_API_KEY first) |
graspful_import_brand |
Yes | Import brand config — required for site to work (set GRASPFUL_API_KEY first) |
graspful_list_courses |
Yes | List org courses (set GRASPFUL_API_KEY first) |
Tools marked "No" for auth work offline — no account needed. Tools marked Yes will fail with a clear error if you haven't authenticated. Run graspful register, then restart MCP with GRASPFUL_API_KEY.
MCP discovery: To check if MCP is active, try calling graspful_validate with any small YAML string. If it responds, MCP is working. If you get a "tool not found" error, fall back to the CLI.
If you haven't already authenticated (see "Authentication" above), do it now before proceeding to import.
CLI:
graspful register --email <email>This creates an account, org, and API key through browser auth. To use MCP tools that require auth, restart the MCP server with GRASPFUL_API_KEY set to the saved key.
The workflow is: academy plan -> course graphs -> fill -> validate -> review -> import.
Before writing any YAML, follow the detailed runbook in docs/adding-a-course.md. Key steps:
- Gather source material (official docs, syllabi, PDFs — not marketing copy)
- Model it as an academy first, even if it starts with one course
- Decompose the academy into foundations -> structures -> operations -> applied judgment
- Build the prerequisite graph (roots -> trunk -> branches -> leaves)
- Write the YAML skeleton (graph first, content second)
- Spend a cycle on the academy landing page so the promise and proof are specific to the learner
- Fill concepts one at a time
- Validate and review
# 1. Scaffold the academy plan
graspful create academy --topic "Your Topic" -o academy.yaml
# 2. Edit the academy plan until source material, learner promise,
# landing-page proof, and course dependencies are specific.
# 3. Scaffold each course knowledge graph
graspful create course --topic "Your Topic" --hours 10 -o course.yaml
# 4. Fill each concept with knowledge points and problems
graspful fill concept course.yaml <concept-id>
# 5. Validate after every edit
graspful validate course.yaml
# 6. Review must score 10/10 to publish
graspful review course.yaml
# 7. Import and publish
graspful import academy.yaml --org <org-slug> --course-dir .Or with MCP tools:
graspful_create_academy(topic: "Your Topic")
graspful_scaffold_course(topic: "Your Topic", estimatedHours: 10)
-> edit the YAML
graspful_fill_concept(yaml: "...", conceptId: "concept-id")
graspful_validate(yaml: "...")
graspful_review_course(yaml: "...")
graspful_import_academy(manifestYaml: "...", courseYamls: { "courses/course.yaml": "..." }, org: "org-slug", publish: true)
Course YAML supports rich media through content blocks on two fields: instructionContent and workedExampleContent. Each is an array of typed blocks.
Image block:
instructionContent:
- type: image
url: https://example.com/photo.jpg
alt: Description for accessibility
caption: Optional caption text
width: 960 # optional, positive integerVideo block:
instructionContent:
- type: video
url: https://youtube.com/watch?v=abc123
title: Video title
caption: Optional captionLink block:
instructionContent:
- type: link
url: https://example.com/reference
title: Link title
description: Optional descriptionCallout block:
instructionContent:
- type: callout
title: Important distinction
body: The explanation text here.Important: instruction and workedExample (the plain text fields) should remain readable as standalone text because they power audio. Put images, diagrams, external references, and video links in the *Content blocks — do not bury URLs in the prose.
When a user asks for images or visual comparisons in a course, use image content blocks with publicly accessible URLs. Every knowledge point can have multiple content blocks.
When building a course from a PDF or document:
- Read the full source material first
- Extract the key concepts, facts, and distinctions
- Map them to a prerequisite graph (what must be learned before what?)
- For visual content (photos, diagrams, comparisons), find or request publicly accessible image URLs and use
imagecontent blocks - Do not copy-paste prose verbatim — rewrite for the lesson pattern (instruction -> worked example -> problems)
Every org needs a brand for the site to work. Registration creates a minimal default, but you should update it with content relevant to the course topic.
Use graspful_create_brand to generate a brand YAML tailored to the course topic,
then import it with graspful_import_brand. This updates the landing page headline,
features, and SEO to match the actual course content.
Important:
- do not leave the landing page as generic niche filler
- the page should name the learner, the outcome, the curriculum shape, and the proof for why this academy is worth joining
- treat landing-page authoring as part of the academy build, not post-launch polish
If the org already has a brand (from registration), importing a new one updates it in place (upsert by slug).
Create a white-label landing page and theme:
graspful create brand --niche tech --topic "Your Topic" --name "My Academy" --org my-org -o brand.yaml
graspful import brand.yamlBrand theme requirements: When creating a brand with custom light/dark theme colors (not using a preset), you must also include:
- A
gradientobject with hex colors:start,mid,end,accent— used for CTA buttons, text gradients, and decorative orbs on marketing pages. - All 18 color fields in both
lightanddark:primary,primaryForeground,secondary,secondaryForeground,accent,accentForeground,background,foreground,card,cardForeground,popover,popoverForeground,muted,mutedForeground,destructive,border,input,ring.
Missing fields will fall back to defaults, but providing them all ensures visual consistency. After importing a brand, run the brand resilience tests:
cd apps/web && bunx vitest run src/lib/brand/__tests__/resolve-db.test.ts src/__tests__/brand/theme-style.test.tsx- CLI or MCP for everything. Browser auth is only for minting credentials; course creation, import, and publishing stay programmatic.
- Validate after every edit. It's offline and fast.
- Fill one concept at a time. Quality is better than filling the whole course at once.
- Review before import. The server rejects courses that fail the quality gate.
- Problem IDs must be globally unique. Use
{concept-id}-{kp-index}-p{problem-index}.
Pass --format json to any CLI command for machine-readable output:
graspful validate course.yaml --format json| Variable | Description |
|---|---|
GRASPFUL_API_KEY |
API key for import/publish (set automatically by register or login) |
GRASPFUL_API_URL |
API base URL (default: https://api.graspful.ai) |
- Frontend: Next.js 16 (App Router), React, Tailwind CSS, shadcn/ui
- Backend: NestJS (TypeScript), Prisma ORM, PostgreSQL (Supabase-hosted)
- Auth: Supabase Auth (JWT)
- Monorepo: Turborepo, bun as package manager
The backend follows Domain-Driven Design with bounded contexts. Each NestJS module owns its domain. Do not leak domain logic across boundaries.
| Context | Module | Aggregate Root | Owns |
|---|---|---|---|
| Knowledge Graph | knowledge-graph/ |
Course | Concepts, KnowledgePoints, PrerequisiteEdges, EncompassingEdges |
| Student Model | student-model/ |
StudentProfile | ConceptState, KPState, mastery, enrollment |
| Diagnostic | diagnostic/ |
DiagnosticSession | BKT engine, MEPE selector, stopping criteria, session persistence |
| Learning Engine | learning-engine/ |
LearningSession | Task selection, frontier, mastery enforcement, remediation |
| Assessment | assessment/ |
Assessment | Problems, answer evaluation, reviews, quizzes |
| Spaced Repetition | spaced-repetition/ |
RepetitionSchedule | FIRe algorithm, review scheduling |
| Gamification | gamification/ |
PlayerProgress | XP, streaks, leaderboards |
- Services call services, not repositories of other modules. If Diagnostic needs mastery data, it calls
StudentStateService, notprisma.studentConceptStatedirectly. - Controllers are thin. Extract, validate, delegate to service, return. No domain logic in controllers.
- Each module owns its Prisma queries. Other modules request data through the owning module's service.
- Cross-context data for the frontend should be composed at the API/controller layer or in a dedicated query service — not by having one domain service reach into another's tables.
- Build:
cd backend && /path/to/tsc -p tsconfig.build.json --outDir dist(nest build has symlink issues with bun) - Run:
TS_NODE_PROJECT=tsconfig.runtime.json node -r tsconfig-paths/register dist/main.js - Dev:
bun run dev(nest start --watch) - Test:
bun run test - Port: 3000
- Dev:
bun run dev(port 3001) - Build:
npx next build - E2E:
cd apps/web && npx playwright test
- Schema:
backend/prisma/schema.prisma - Migrate:
cd backend && npx prisma migrate dev --name <name> - Generate:
npx prisma generate(runs automatically after migrate)
- Use
bunnotnpm - snake_case for DB columns (Prisma
@@map), camelCase for TypeScript - All Prisma models use
@db.Uuidfor IDs and@db.Timestamptzfor dates - Tests: Jest for backend unit tests, Playwright for e2e
- E2E helpers:
apps/web/e2e/helpers/auth.ts—signUpTestUser()creates fresh users - Brand cookie:
dev-brand-overrideselects org in dev
Every live site page and API endpoint MUST have an e2e test. This is non-negotiable — bread-and-butter functionality that users depend on must have regression coverage.
- All pages render — every route under
(marketing)and(app)must have a smoke test verifying it returns 200 and renders its heading. Seee2e/docs-smoke.spec.tsfor the pattern. - Auth flows — sign-up, sign-in, sign-out, email confirmation callback, org provisioning
- Creator flows — API key CRUD, course import, brand config import, course publish
- CLI registration —
graspful registeropens browser auth, then stores an API key locally for later CLI/MCP use - API provisioning —
POST /auth/provisioncreates personal org for web UI sign-ups - Learner flows — browse, enroll, diagnostic, study session
- Add an e2e test in the same PR
- If it's a new doc page, add it to the
DOCS_PAGESarray ine2e/docs-smoke.spec.ts - If it's a new API endpoint, add API-level tests using the
helpers/api-auth.tshelpers
- Documentation: https://graspful.ai/docs
- CLI reference: https://graspful.ai/docs/cli
- MCP setup: https://graspful.ai/docs/mcp
- Course YAML schema: https://graspful.ai/docs/course-schema
- Brand YAML schema: https://graspful.ai/docs/brand-schema
- Full docs for LLMs: https://graspful.ai/llms-full.txt