Mathew — a wiki for mathematics
A collaborative wiki of mathematical concepts. Articles are written in Markdown with LaTeX (KaTeX), stored in MongoDB behind an Express API, and rendered by a Nuxt 4 app as an open book with a table of contents, a table of topics, a link graph, and a review-then-publish flow for edits.
The interface speaks five languages — English, Spanish, Catalan, French, German — chosen per account, and articles themselves are multilingual: one article can exist in several languages, each with its own address.
mathew/
├── dev.sh # start/stop both dev servers together
├── docker-compose.yml # nginx + frontend + backend (prebuilt images)
├── nginx.conf # reverse proxy: / → frontend, /api/ → backend
├── backend/ # Express 5 + Mongoose API (port 5000)
└── frontend/ # Nuxt 4 / Vue 3 wiki UI (port 3000)
Quick start
Requires MongoDB on localhost:27017 (database mathew) and Node 20+.
# 1. API
cd backend
npm install
cp .env.example .env # then set JWT_SECRET
npm run seed # optional: sample articles with LaTeX
npm run dev # → http://localhost:5000
# 2. UI
cd ../frontend
npm install
npm run dev # → http://localhost:3000
Or run both together from the repo root:
./dev.sh # both dev servers in the foreground (Ctrl+C stops both)
./dev.sh start|stop|restart|status
./dev.sh logs [all|backend|frontend]
Scripts
| Where | Command | What it does |
|---|---|---|
| root | ./dev.sh [dev] |
Both dev servers, foreground, tagged output, one Ctrl+C stops both |
backend/ |
npm run dev |
API with node --watch (no build step) |
backend/ |
npm start |
API without watch |
backend/ |
npm run seed |
A handful of sample articles |
backend/ |
npm run seed:bulk |
100 cross-linked articles for load-testing the UI and graph (idempotent) |
frontend/ |
npm run dev / build / preview |
Nuxt dev server, production build, preview of the build |
There is no test suite, linter, or typecheck script configured.
Configuration
| Where | Variable | Default | Meaning |
|---|---|---|---|
backend/.env |
PORT |
5000 |
API port |
backend/.env |
MONGO_URI |
mongodb://127.0.0.1:27017/mathew |
Mongo connection |
backend/.env |
JWT_SECRET |
(insecure dev key) | Signs session tokens — set this in production |
backend/.env |
AUTH_TOKEN_TTL |
7d |
How long a session lasts |
frontend env |
NUXT_PUBLIC_API_BASE |
http://localhost:5000 |
Where the UI calls the API |
How it works
Reading is open; writing takes an account. Anyone can register, and the first account
ever created becomes the admin (on a database whose accounts all predate roles, the oldest
one gets it at startup). The admin hands the role on or takes it back from /admin; nobody
may change their own role, so the dashboard always has a door.
- An admin's save applies immediately.
- A member's save is answered
202and filed as an edit proposal instead — the live article does not move. Saving again while a proposal waits revises it, and each proposal keeps a snapshot of the article as it read when the request was sent, so the desk can show a diff and warn when the page has moved since. - Proposals include new articles: a request for a page the wiki does not have yet keeps the address it asks for and appears only when approved.
- Approving or rejecting happens at
/admin/proposals, and approval runs the same code an admin's direct write runs — so a retitle re-slugs, and a language the article already has in another version is refused. - Deleting is admin-only, and it settles that article's waiting proposals with it.
Articles name no author, so they outlive the account that wrote them.
Languages
Two language ideas, deliberately apart:
- The interface language (
en,es,ca,fr,de) is a preference and never part of a URL. Precedence: the signed-in account's choice → themathew-localecookie → the browser → English. Pick it in the account popup. - The article language appears in the address:
/wiki/<lang>/<identity>. Every version of an article has its own address, English included —/wiki/en/bayes-theoremand/wiki/fr/bayes-theoremare one article two ways. The short form/wiki/<slug>redirects onto the version the reader should see: their own language if the article speaks it, English failing that.
Switching an article's language turns the page of the open book rather than opening a second one, and "recently viewed" remembers article + language as a single entry.
Themes
Light and dark, plus eight accent colours, chosen in the settings popup and applied before
first paint so there is no flash. Colours are CSS custom properties swapped by
data-theme / data-accent on <html>.
Pages
| Route | What it is |
|---|---|
/ |
Search, recently viewed, a way into the shelf |
/articles |
The shelf, filed by topic — one folder per topic, opening onto a batch-at-a-time grid of cards; language and topic filters are applied by the API |
/graph |
The wiki's link map: one node per article, one edge per linked pair |
/wiki/<lang>/<slug> |
One language version of an article, drawn as a page of the book with its table of contents |
/wiki/<slug> |
The short form, naming no language — turned onto the version the slug speaks |
/wiki/<slug>/edit |
Markdown editor with live preview (Write / Split / Preview) |
/new |
Create a new article |
/admin |
Accounts and roles (admin only) |
/admin/proposals |
The edit-request desk: diff, approve, reject (admin only) |
Writing articles
GitHub-ish Markdown — headings, tables, lists, blockquotes, fenced code with syntax
highlighting — plus LaTeX: $E = mc^2$ inline, $$ … $$ in display mode. Number-set
macros \R, \N, \Z, \Q, \C are available. Raw HTML in an article is escaped, and
everything rendered is sanitized with DOMPurify.
Link between articles with wiki links — [[Bayes' theorem]], [[Fourier series#definition]],
[[Zeno's paradoxes|Zeno]] — or with a normal Markdown link to /wiki/<slug> or
/wiki/<lang>/<slug>. Both feed the graph view.
Shortcuts: / focuses search on the home page, Ctrl/Cmd+K opens the search popup,
Ctrl/Cmd+S saves in the editor. Changing an article's title can change its slug — old
links then 404, as no redirects are kept.
API
GET /health answers { status, mongo }. Everything below lives under /api.
Reads are public; writes need Authorization: Bearer <token> from /auth/login or
/auth/register. All errors answer { message }.
Accounts
| Method | Endpoint | Notes |
|---|---|---|
| POST | /auth/register |
Creates the account and returns its session; the first account ever is an admin |
| POST | /auth/login |
A wrong name and a wrong password answer the same way |
| GET | /auth/me |
Who a token belongs to |
| PUT | /auth/me |
Rename |
| PUT | /auth/password |
Needs the current password |
| PUT | /auth/locale |
Interface language — one of en es ca fr de |
| PUT | /auth/avatar |
A small data URL; "" clears it |
Articles
Articles are addressed by slug, not by Mongo id.
| Method | Endpoint | Notes |
|---|---|---|
| GET | /articles |
{ articles, total, hasMore }, paged by article — ?q=, ?tag=, ?lang=, ?limit=, ?offset=; ?prefer= picks which version stands for an article, ?preferFirst ranks those versions first; no limit means the whole shelf |
| GET | /articles/facets |
{ total, topics, languages } counted over the whole shelf |
| GET | /articles/random |
One random article |
| GET | /articles/graph |
{ nodes, edges } — the link map built from wiki and Markdown links |
| GET | /articles/slug/:slug |
One article |
| POST | /articles |
Create (title required; the slug is generated and uniquified). Admin applies; a member is answered 202 { proposal } |
| PUT | /articles/slug/:slug |
Update. Admin applies; a member is answered 202 { proposal } |
| DELETE | /articles/slug/:slug |
Admin only |
Administration
Everything behind requireAuth, requireAdmin.
| Method | Endpoint | Notes |
|---|---|---|
| GET | /admin/users |
Every account, newest first |
| PUT | /admin/users/:id/role |
role is admin or member; never your own |
| DELETE | /admin/users/:id |
Close an account — never your own; its articles stay |
| GET | /admin/proposals |
Every request, decided ones included, with its diff material |
| GET | /admin/proposals/count |
Just { pending } — what the sidebar's badge wears |
| PUT | /admin/proposals/:id/approve |
Writes the change; a conflict refuses it and the request keeps waiting |
| PUT | /admin/proposals/:id/reject |
Refuses; the page stays put |
/api/items is a small demo resource (CRUD over one schema) left in as a reference for how
a router, model and error path fit together.
Deploy
docker-compose.yml runs nginx in front of prebuilt frontend and backend images: nginx
listens on 3000 and routes /api/ to the backend and everything else to the Nuxt server,
passing the standard forwarded headers. Point NUXT_PUBLIC_API_BASE and MONGO_URI at the
deployment's real addresses and set a real JWT_SECRET — the fallback key is a development
convenience only, and anyone holding it can mint a session.
Notes for contributors
AGENTS.md is the working map of the codebase: file-by-file layout, the
backend and frontend conventions, and the gotchas that are easy to get wrong (article
identity vs. document slug, versioned addresses, MongoDB's reserved language field,
localization rules).
License
MIT.