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.
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
}
}
]idYeslabelYesdomainYessubdomainOptionalgrading_typeYesprerequisitesYesmastery_thresholdYesmastery_threshold.streakYesmastery_threshold.avg_time_secondsYes| Field | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Unique dot-separated identifier (e.g., arith.add.single) |
| label | string | Yes | Human-readable name displayed in the UI |
| domain | string | Yes | One of the 16 domain categories |
| subdomain | string | Optional | Nested grouping within a domain |
| grading_type | enum | Yes | numeric for arithmetic, polynomial/expression for algebra (uses SymPy), multiple_choice, comparison, ordering |
| prerequisites | string[] | Yes | Concept IDs that must be mastered first |
| mastery_threshold | object | Yes | Object with streak (int) and avg_time_seconds (float) fields |
| mastery_threshold.streak | number | Yes | Consecutive correct answers required for mastery |
| mastery_threshold.avg_time_seconds | number | Yes | Maximum acceptable average response time in seconds |
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
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
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.
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 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
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:
Deployment
Mathua is deployed as two services:
The graph validator
A validator runs on every pull request. It checks two invariants before any merge can happen:
go run scripts/validate_graph.go
Submitting a pull request
Once everything passes locally, here's the full checklist:
Reviews usually happen within a few days. If a week passes with no response, feel free to ping the thread. We read every PR.