Community

Contributing to Mathua

Mathua is a full-stack project: a Go backend with a Next.js frontend, a scheduling engine, and a concept graph. Every generator, every diagram, every API route was built by someone who wanted to help others learn math. Here's how to join them.

View source on GitHub

Getting started

You'll need Go 1.25+ and Node.js 18+. Clone the repo:

git clone https://github.com/chuma-beep/mathua.git
cd mathua

Running the full stack

The backend serves the API on port 8080. The frontend (Next.js) runs on port 3000 and proxies API calls through the NEXT_PUBLIC_API_URL env var:

# Terminal 1 · Go backend
go build ./cmd/mathua
./mathua --serve --port 8080

# Terminal 2 · Next.js frontend
cd web/next-app
npm install
npm run dev

For quick local testing without authentication, use ./mathua --serve --no-auth. In that mode the session page will skip login and route you straight to practice.

Project layout

Key directories you'll work in:

data/concepts/Per-domain JSON files defining the concept graph.
internal/generator/Per-domain Go packages that produce problems.
internal/server/HTTP API server with route handlers.
internal/storage/Repository interface + SQLite implementation.
internal/engine/Scheduling engine, session logic, diagnostics.
internal/auth/JWT-based authentication service.
web/next-app/app/Next.js App Router pages and layouts.
web/next-app/components/React components (diagrams, heatmaps, etc).
web/next-app/lib/API client, auth helpers, shared utilities.

Step 1: Define a new concept

Concepts live in per-domain JSON files under data/concepts/. Add your new concept as an entry in the array for the matching domain (e.g., add an arithmetic concept to arithmetic.json):

[
  { "id": "arith.add.single", "label": "Single-digit addition", ... },
  {
    "id":                "arith.mult.tables",
    "label":             "Multiplication tables 1–12",
    "domain":            "arithmetic",
    "subdomain":         "arithmetic.multiplication",
    "grading_type":      "numeric",
    "prerequisites":     ["arith.add.multi"],
    "mastery_threshold": {
      "streak":           7,
      "avg_time_seconds": 6.0
    }
  }
]
idYes
string
Unique dot-separated identifier (e.g., arith.add.single)
labelYes
string
Human-readable name displayed in the UI
domainYes
string
One of the 16 domain categories
subdomainOptional
string
Nested grouping within a domain
grading_typeYes
enum
numeric for arithmetic, polynomial/expression for algebra (uses SymPy), multiple_choice, comparison, ordering
prerequisitesYes
string[]
Concept IDs that must be mastered first
mastery_thresholdYes
object
Object with streak (int) and avg_time_seconds (float) fields
mastery_threshold.streakYes
number
Consecutive correct answers required for mastery
mastery_threshold.avg_time_secondsYes
number
Maximum acceptable average response time in seconds
The prerequisites list is the most important field. What must a student absolutely know before attempting this? If in doubt, add the prerequisite: the graph validator will catch cycles.

Step 2: Teach Mathua to ask questions

A generator is a Go function that produces a unique problem every time it's called. There is no static question bank: every problem is built on demand. Generators live in internal/generator/[domain]/ and implement the Generator interface.

package arithmetic

import (
    "crypto/rand"
    "fmt"
    "math/big"
    "strconv"
    "github.com/chuma-beep/mathua/internal/generator"
)

type AddSingleGen struct{}

func (g *AddSingleGen) Generate(ctx generator.GeneratorContext) generator.Problem {
    max := int(5 + ctx.Difficulty*4)
    a, _ := rand.Int(rand.Reader, big.NewInt(int64(max)))
    b, _ := rand.Int(rand.Reader, big.NewInt(int64(max)))
    ai, bi := int(a.Int64())+2, int(b.Int64())+2
    ans := ai + bi
    return generator.Problem{
        Question:    fmt.Sprintf("%d + %d = ?", ai, bi),
        Answer:      strconv.Itoa(ans),
        Explanation: fmt.Sprintf("%d + %d = %d", ai, bi, ans),
    }
}

Guidelines

·Use the difficulty parameter to scale operand sizes. At 0.0, trivial. At 1.0, challenging for someone at that level.
·Always return an Explanation: it is shown when a student asks to see the solution.
·Use crypto/rand or math/rand with a seeded source. No hardcoded problems.
·Stay deterministic with respect to difficulty. A student should not get a meaningfully harder problem at the same difficulty.
·For non-standard answer formats (e.g., "5 R 3" in division), implement the GradedGenerator interface instead.

Step 3: Prove it works

Every generator needs a fuzz test that asserts 1 000 valid samples. This catches edge cases (division by zero, negative operand ranges, malformed output) before a student ever sees them.

func TestAddSingleGen(t *testing.T) {
    g := &AddSingleGen{}
    for i := 0; i < 1000; i++ {
        p := g.Generate(rand.Float64())
        assert.NotEmpty(t, p.Question)
        assert.NotEmpty(t, p.Answer)
        assert.NotEmpty(t, p.Explanation)
        assert.True(t, evaluateNumeric(p))
    }
}

Run your tests before opening a PR:

go test ./internal/generator/... -count 1000

Step 4: Write a lesson (optional)

Mathua includes a built-in lesson system. Each concept can have an associated lesson: a markdown file that teaches the material, rendered inside the app as a sidebar panel alongside practice problems.

Lessons live under data/lessons/ and are registered in data/lessons/lessons.json, which maps concept IDs to file paths:

{ "concept_id": "calc.deriv.power_rule", "source": "advanced/polynomials/polynomials.md" }

Multiple concepts can share a single lesson file. Content is written in Markdown with LaTeX via \\( ... \\) or \\[ ... \\] delimiters.

Frontend contributions

The frontend is a Next.js App Router application under web/next-app/. It's designed with a brutalist aesthetic: monospace typography, minimal chrome, no rounded corners, and a terminal-calibrated color palette.

Design system

·Typeface: IBM Plex Mono for code/UI, IBM Plex Serif for body/headings. Loaded via next/font as CSS variables (--font-ibm-plex-mono, --font-ibm-plex-serif).
·Colors: oklch color space as CSS custom properties (--bg, --surface, --text-primary, --accent-blue, --border, etc). Supports light/dark via class="dark" on <html>.
·Borders: 0.5px solid. No rounded corners (rounded-none everywhere). Zero box-shadow on interactive elements.
·Spacing: 1rem/4px base grid. Sections use py-20 (5rem). Cards use gap-3 (0.75rem) or gap-4 (1rem).
·Font sizes: body 0.95rem, headings scale from 1rem to 1.9rem, mono 13px for code, 12px for labels.

React Flow diagrams

Architecture, scheduler, and student model diagrams use @xyflow/react (React Flow v12). All diagrams share FlowDiagram as a common wrapper in components/FlowDiagram.tsx.

·Use useNodesState + useEdgesState + onNodesChange + onEdgesChange for every diagram. Without these handlers, node dragging is silently ignored.
·When adding custom node types, define them outside the component body (otherwise nodes remount on every render).
·For diagrams that need pan/zoom, set allowZoom=true and do not pass restrictive interaction props; the Controls lock button toggles the React Flow store directly.
·Background dots use var(--border-strong) at size=1 for visibility without distraction.
·Node colors come from the shared themeColors object in FlowDiagram.tsx; use those instead of hardcoding.

Components & pages

Components live in components/. Pages live in app/ under their route segment (e.g., app/profile/page.tsx). The project uses static export (output: 'export'), so all pages are client-rendered with 'use client'.

·API calls go through lib/api.ts. Auth headers come from lib/auth.ts (JWT in localStorage).
·Use the motion-safe animation classes for transitions (fadeIn, ascii-reveal). Avoid heavy animation libraries.
·Profile pages, heatmaps, and stats components consume data from GET /api/activity, /api/scores, and /api/weaknesses.
·UI primitives live in components/ui/ (Radix/shadcn: sidebar, sheet, button, switch, tooltip — used by AppSidebar on Profile), plus React Flow for diagrams, sonner for toasts, lucide-react for icons. Reuse these — do not add new UI kits.

API server contributions

The HTTP server is in internal/server/server.go. It's a standard library net/http server with a custom ServeMux. All routes are registered in Register().

Adding a new endpoint

·Define your handler method on *Server (e.g., handleSomething).
·Register it in Register() with mux.HandleFunc("/api/your-path", logRequest(cors(s.authMiddleware(s.handleSomething)))).
·If the endpoint needs auth, studentID is available from r.Context().Value(authStudentKey{}).
·If the endpoint should work without auth, check s.auth == nil and handle both paths.
·All endpoints set Content-Type: application/json via writeJSON(). Use decodeJSON() and writeError() for request/error handling.
·POST endpoints check r.Method != http.MethodPost and return 405.
·CORS is handled automatically by the cors middleware wrapper; no extra config needed.

Storage layer

Data access goes through the storage.Repository interface in internal/storage/store.go. The SQLite implementation is in internal/storage/sqlite.go. When adding a new query:

·Add the method signature to the Repository interface first.
·Implement it in sqlite.go using the database/sql package.
·The server accesses the repo via s.repo.
·CGO is required (mattn/go-sqlite3). The Dockerfile sets CGO_ENABLED=1.

Deployment

Mathua is deployed as two services:

·Backend: Go binary on Fly.io (Dockerfile, fly.toml). Requires a persistent volume for SQLite (mathua_data at /data) and a JWT_SECRET environment variable.
·Frontend: Static export on Vercel via GitHub auto-deploy. The NEXT_PUBLIC_API_URL env var points to the Fly.io backend. Set in .env.production.
·Local development: Backend on :8080, frontend on :3000. NEXT_PUBLIC_API_URL in .env.local should be http://localhost:8080.
·The Dockerfile is multi-stage: golang:1.26-bookworm builder → debian:bookworm-slim runtime. CGO_ENABLED=1 is required for SQLite.

The graph validator

A validator runs on every pull request. It checks two invariants before any merge can happen:

1. No cycles: concept A cannot require B while B requires A.
2. No orphans: every prerequisite must exist in the graph.
go run scripts/validate_graph.go

Submitting a pull request

Once everything passes locally, here's the full checklist:

1. Add the concept to the appropriate domain file in data/concepts/ with correct prerequisites.
2. Write the Go generator in the appropriate domain subdirectory under internal/generator/.
3. Write the fuzz test with 1 000 samples.
4. (Optional) Write a lesson and register it in data/lessons/lessons.json.
5. Run go test ./... and go run scripts/validate_graph.go locally.
6. For frontend changes: run npm run build in web/next-app/ to verify the static export compiles.
7. For API changes: run go vet ./internal/... to check for issues.
8. Open a PR. The CI pipeline runs the validator and all tests automatically.
9. A maintainer reviews the concept ordering, thresholds, generator quality, and UI changes.
LOADING WORKFLOW

Reviews usually happen within a few days. If a week passes with no response, feel free to ping the thread. We read every PR.

Design conventions

Backend

·Concept IDs follow domain.subdomain.descriptor: lower case, no spaces.
·Mastery thresholds are pragmatic. Single-digit addition should require faster response (6–8 s) than multi-digit multiplication (15–20 s).
·Subdomains group related concepts. If a domain grows past 15 concepts, consider introducing subdomains.
·Generators accept a generator.GeneratorContext with Difficulty (0.0–1.0) and Seed (int64). Use ctx.Seed for deterministic generation; this enables question replay, regression suites, and A/B testing.
·Difficulty scaling should be linear where sensible. The jump from 0.0 to 1.0 should feel meaningful, not extreme.
·Grading types: use numeric for arithmetic, polynomial/expression for algebra (routes through SymPy), and comparison/ordering/multiple_choice for structured answers.

Frontend

·Colors only through CSS variables (var(--accent-blue), var(--border), etc). Never hardcode hex/rgb.
·Typography: var(--font-ibm-plex-mono) for code, var(--font-ibm-plex-serif) for prose. Loaded via next/font CSS variables.
·Borders: border-[0.5px] with var(--border) / var(--border-strong). rounded-none on all interactive elements.
·No box-shadow on buttons or inputs. Use border changes or background transitions for hover states.
·Animations: use the @keyframes defined in tailwind.config.js (ascii-reveal, fadeIn, progress-fill). Keep transitions under 300ms.
·Diagrams: follow the React Flow conventions above. Use dagre for automatic layout, themeColors for node styling.
·Read the full design system in DESIGN.md for color palette, typography scales, spacing, and component patterns.
Read the full design system in DESIGN.md for color palette, typography, spacing, and component patterns.