Caissa Codex (https://chesscodex.org) is a free encyclopedia of chess openings: 3,690 named lines, each with its own page, plus pages for every ECO code and a few rankings, about 4,000 URLs in total. It runs on a Raspberry Pi 5 with 1 GB of RAM in my home. Here is what makes that work.
Boring stack, on purpose
PHP 8.4 and SQLite behind nginx and PHP-FPM. No framework, no build step, no Node on the server. The front controller is one file with a route table, the views are plain PHP templates, and a small PHP script minifies the CSS and JavaScript. A page is a few prepared SQLite queries and a template, which a Pi handles easily.
Let the edge do the work
Anonymous pages send Cache-Control: public, s-maxage=600, and Cloudflare keeps them for 10 minutes. Most requests never reach the Pi, and PageSpeed scores stay around 100.
No open ports
The Pi is reachable only through a Cloudflare Tunnel: the router has no port forwarding, and SSH goes through Cloudflare Access with keys only.
Living with a rate-limited API
The statistics come from the Lichess opening explorer, which in practice allows about 20 requests a minute. So the site never fans out requests: the numbers are cached in SQLite and refreshed in the background one request at a time, and bulk data (results by rating band for the 1,500 most-played lines) is collected by slow command-line jobs.
Stockfish on the Pi
Every line's final position was evaluated by Stockfish 19 at depth 30 on the same Pi, and the results sit in a table next to the statistics. That makes comparisons like this possible: Stockfish gives White +2.03 after the Stafford Gambit, yet Black scores 50.8% in 4.6 million Lichess games.
Security basics
A strict Content Security Policy without inline scripts (all JavaScript lives in files), escaped output everywhere, and prepared statements for every query.
Top comments (1)
We run the same tunnel, no-open-ports shape on a bigger box, and the never-fan-out rule is the part people skip — one request at a time against a rate-limited upstream beats the burst-and-429 cycle every time. What does a cold page cost you on 1 GB — is the SQLite cache small enough to stay page-cached, or do the first hits after a restart hit disk?