03 / 05Case study
Wikiverse
Full-stack web encyclopedia
An encyclopedia built from scratch, without MediaWiki: 63 articles with revision history, word-by-word diffs, talk pages, red links and full-text search in PostgreSQL, in production on its own domain.
- My role
- Product, interface, full-stack development, content and deployment
- Stack
- React 19 · TypeScript · Django REST Framework · PostgreSQL · Redis · Railway
- Status
- In production on Railway · own domain
Scope
- 608tests in CI, from the backend to Playwright
- 63articles written, with 428 references
- 36API routes documented with OpenAPI
The challenge
In a wiki anyone can change the text, and readers need to trust it and find their way across many articles: traceability and navigation are the product; showing articles is the easy part.
- What I built
- I turned my 2024 CS50W project into a complete encyclopedia — revisions, version diffs, talk pages, PostgreSQL search, an editor with live preview and content security — took it to production with 608 tests in CI and wrote all 63 articles.
- Key technical decision
- Every save is an immutable revision, and the search index is maintained by a PostgreSQL trigger: neither history nor search depends on the code remembering to update them.
Design decisions

- Problem
- Text that anyone can edit loses trust if you can't see who changed what.
- Decision
- Every save is an immutable revision with author, summary and delta; any two can be diffed word by word.

- Problem
- Diffing a long article word by word in a single pass took tens of seconds.
- Decision
- The diff compares lines first and only then words, inside the lines that changed.

- Problem
- A search index that the code has to remember to update eventually falls out of date.
- Decision
- A PostgreSQL trigger recomputes the weighted vector of title, summary and body on every change.

- Problem
- A search with a typo finds nothing, and the reader leaves.
- Decision
- Suggestions as you type and trigram similarity as a second attempt; “entrpy” finds Entropy.

- Problem
- Opening every link to see what it's about interrupts reading.
- Decision
- On hover, a card shows the linked article's short description and excerpt.

- Problem
- A link to an article that doesn't exist yet usually breaks or gets hidden.
- Decision
- It shows in red, opens the editor with the title ready and turns blue once someone writes it.

- Problem
- A preview with a different renderer ends up showing something other than what gets published.
- Decision
- The editor previews with the same Markdown pipeline as the article.

- Problem
- Contents, article and tools in three columns don't fit on a phone.
- Decision
- Below 960 px the contents move into the header and the infobox stacks; nothing overflows at 390 px.
System
Pick a module: its path lights up.Pick a module: its decision appears below.
A featured article, trivia and anniversaries drawn from editable blocks; the main page is cached for a set time.
React 19 · TanStack Query 5
Below 960 px the contents move into the header and the infobox stacks; no screen overflows at 390 px.
Tailwind CSS 4.3
Markdown is rendered on the client with a custom plugin for wikilinks, footnotes and red links, with no raw HTML.
react-markdown 9 · remark-gfm 4 · Custom remark plugin
The query, filter and sort live in the URL: a search can be linked and the back button works.
React Router 7 · TanStack Query 5
Any two revisions can be compared side by side or in one column, with each change's byte delta.
React 19
Edits and talk messages in a single feed, with every filter in the URL and an RSS feed.
React Router 7
Loaded only when used, previews with the same renderer as the article and saves drafts without restoring them on its own.
React.lazy · localStorage
36 routes documented with OpenAPI; 30-minute JWT with a refresh token that rotates and is revoked on sign-out.
Django REST Framework 3.17 · SimpleJWT 5.5 · drf-spectacular 0.30
One immutable revision per save, with parent, author and delta; a save without changes creates none.
Django 5.2
websearch_to_tsquery with ranking, trigram when there are no results and snippets escaped before highlighting.
PostgreSQL full-text · pg_trgm
nginx sends only link-preview bots to Django, which answers with Open Graph and JSON-LD; people get the SPA.
Django 5.2 · Open Graph
Stats, main page, suggestions, diffs and rate limits: losing Redis costs performance, not data.
Redis · django-redis 5.4
63 articles in 21 modules validated against 31 rules; seeding twice produces exactly the same history.
Custom seed command
A trigger maintains the search vector with A, B and C weights; Postgres-only features are chosen at call time, not import time.
PostgreSQL 18 on Railway · tsvector + GIN
Frontend, backend, PostgreSQL and Redis as four services; it deploys on every push to main.
Railway · Docker
Serves the SPA, proxies /api and emits the same CSP as Django; index.html uncached and hashed assets cached forever.
nginx 1.27
608 tests, plus complete migrations, a warning-free OpenAPI schema and the corpus validated before merging.
GitHub Actions · pytest · Vitest · Playwright
Images only from Wikimedia Commons, with author and license; the allowlist is enforced on the server too.
Wikimedia Commons
Client
Main page
A featured article, trivia and anniversaries drawn from editable blocks; the main page is cached for a set time.
- Technologies
- React 19 · TanStack Query 5
- Sends to
- REST API
Technologies
48 tools across 10 areas. In the system, each module lists its own.
Languages
- TypeScript 5.9
- Python 3.13
- SQL (PL/pgSQL)
- CSS
Frontend
- React 19.2
- React Router 7.18
- Vite 6.4
- react-markdown 9
- remark-gfm 4
- Custom remark plugin
Interface
- Tailwind CSS 4.3
- lucide-react
- Source Serif 4
- Inter
- IBM Plex Mono
- Light and dark themes
Client state and data
- TanStack Query 5
- Zustand 5
- Axios 1.18
Backend and API
- Django 5.2
- Django REST Framework 3.17
- SimpleJWT 5.5
- drf-spectacular 0.30
- django-filter 25
Data and search
- PostgreSQL 16 and 18
- tsvector + GIN
- pg_trgm
- unaccent
- psycopg 3
- SQLite in development
Cache
- Redis 7
- django-redis 5.4
Server and deployment
- nginx 1.27
- Gunicorn 23
- WhiteNoise 6.12
- Docker Compose
- Railway
Quality and CI
- pytest 8.4
- Vitest 4.1
- Testing Library
- Playwright 1.61
- Ruff
- ESLint 9
- GitHub Actions
Tooling
- Node.js 22
- Makefile
- Custom management commands
- Git
Verifiable results
- In production at wikiverse.jonasjavier.dev with nginx, Django, PostgreSQL and Redis on Railway
- Immutable revisions, history and word-by-word diffs between any two versions
- Full-text search in PostgreSQL with ranking, autocomplete and typo tolerance
- Red links, two-way footnotes and a preview of every link on hover
- An editor with live preview and an infobox edited row by row
- 608 tests in CI, a strict CSP and no raw HTML anywhere in the content
Tour by module
18 screens across 5 modules.
Main page and articles4 screens
Browse and search5 screens

Hovering over a link shows a summary, without leaving what you're reading.

Search suggests titles as you type and tolerates typos.

Full-text search in PostgreSQL, with ranking and highlighted matches.

The whole catalog can be browsed by letter, category, recency or length.

Categories gather main and secondary articles with an alphabetical index.
History and changes2 screens
Editing and API3 screens
On the phone4 screens
6 min read
The full case
Context
Wikiverse started in April 2024 as the "Wiki" project for CS50W, Harvard's web programming course, which I submitted and passed: Django with templates and four entries stored as Markdown files. In June 2026 I rebuilt it as a Django REST Framework API with a React app, and in September I turned it into a complete encyclopedia and took it to production.
The visual design takes Wikipedia as a reference, with its own typefaces, colors and layout. I wrote all 63 articles — about 75,000 words on science, geography and computing — and checked their 428 references.
The problem
A wiki has to solve two things at once: anyone can change the text, and the reader has to be able to trust it and find their way across many articles. That's why traceability — who changed what, and when — and navigation — links, categories and search — are the product. Showing articles is the easy part.
My role
The project is mine from start to finish: the product, the interface, the data model, the API, search, security, testing, continuous integration, deployment, documentation and content.
From CS50W to an encyclopedia
The brief asked for a page per entry, an index, substring search, creating and editing in Markdown, and a random page. Wikiverse keeps those features and adds what makes a wiki a wiki:
- immutable revisions and word-by-word diffs between any two versions;
- talk pages with nested replies;
- recent changes with filters in the URL and an RSS feed;
- a watchlist, JWT accounts and page protection;
- red links that turn blue on their own when someone writes the article;
- two-way footnotes and a preview of every link;
- full-text search with autocomplete and typo tolerance;
- a relational model in PostgreSQL instead of loose files.
Constraints
- Two databases. Everything works on SQLite and PostgreSQL; Postgres-only features are chosen at call time, never at import time.
- No raw HTML. No
dangerouslySetInnerHTMLand no HTML inside the Markdown: a test fails if it ever comes back. - A backend that's public too. On Railway the backend answers on its own host, outside nginx, so Django emits the same security headers.
- Services that sleep. After a while without visits, the first request takes a few seconds.
Architecture decisions
- Revisions.
Articlestores the current state, and every save creates an immutableRevisionwith its parent, author, summary, size and delta. A save with no changes creates no revision, and deleting keeps the history. - A link graph. It's rebuilt on every save. A target that doesn't exist stays as a red link with its title, and turns blue the day someone writes it.
- A single renderer. Articles, talk pages, infoboxes, references and the editor preview all go through the same Markdown pipeline, with a custom plugin for wikilinks and notes.
- Search. A PostgreSQL trigger maintains the weighted vector for the title, summary and body. If a query finds nothing, it's retried with trigram similarity and a title is suggested.
- Two-stage diffs. Lines first, then words, only inside the lines that changed, with limits that truncate rather than time out.
- Redis caching. Stats, the main page, suggestions and historical diffs, which never expire because they're immutable.
Security
- The same CSP comes from nginx and from Django: scripts only from the site's own origin, no embedding the site, and images only from Wikimedia.
- Search matches are marked with control delimiters, escaped, and only then turned into highlighting.
- Nine configurable rate limits; sign-in and sign-up are also limited per user, so switching IPs doesn't reset them.
- 30-minute JWT with a two-day refresh token that rotates and is revoked on sign-out.
- Fonts are served from the site itself.
A SPA that shares well
A single-page app doesn't give social networks preview cards. nginx recognizes link-preview bots and sends only them to a Django view with Open Graph and JSON-LD; people and search engines get the app. The sitemap and robots.txt come from the same origin.
Reproducible data
The corpus lives in 21 modules and is validated against 31 rules before anything is written to the database. Each article's history — two to nine revisions — is derived from its title, so seeding twice produces exactly the same result. CI checks it together with the migrations and the API schema.
Challenges
- An empty database with everything green. Production went out with no articles and the health checks didn't catch it. It was seeded, and the release process now requires checking the content, not just the health.
- A quadratic diff. Diffing a long text word by word in a single pass took tens of seconds; the two stages solved it.
- Five conflicting specs. They contradicted each other on 35 points, and a single decisions document became the source of truth.
- A proxy tied to a name. The backend host was hard-coded in the nginx config; renaming the service broke the site with no error at all. It moved to environment variables.
- A password in the history. A development admin password ended up in git; it was revoked with a migration, and the account is now created with a custom command.
What's missing
The interface is English-only. There are 57 planned articles that are red links today, lead images are served at full resolution instead of as thumbnails, the highest protection level on a new page isn't restricted to staff yet, and error monitoring is wired up but switched off. "On this day" shows eight fixed anniversaries, not the current day's.
About the content and screenshots
The article text is mine and published under CC BY 4.0; the images come from Wikimedia Commons, each with its author and license. Users, page views, talk threads and edit history are generated by the project's seed: they aren't real traffic or real contributors. The screenshots were taken on September 28, 2026, with the production stack running locally on the latest code, and the link-preview one in production. Screens with any visible defect were left out.
What I learned
A green health check doesn't prove the product works: you have to check the content. Pinning down a written contract before building avoided reopening decisions. And security that tests itself — a test that fails if raw HTML comes back — holds up better than security reviewed by hand.
Current status
In production at wikiverse.jonasjavier.dev: nginx and Django on Railway, with PostgreSQL and Redis. If it hasn't had visits for a while, the first load can take a few seconds. The repository is public under the MIT license, and continuous integration passes 608 tests: 333 backend, 258 Vitest and 17 Playwright journeys.








