Pholani (pronounced Po-LAH-nee, isiZulu for "look") is a small, safe, browser-based Pascal/Delphi IDE built for Grade-12 students. Write Pascal in your browser, click run, see your program come to life β no Delphi install, no terminal gymnastics.
- Why Pholani
- Quick start
- Install demo
readlnworks in the browser- Architecture
- Caching
- Security model
- Configuration
- Tests
- Project layout
- Further docs
- License
Most Pascal tooling for high school is either Windows-only Delphi or a raw fpc terminal. Both lose students. Pholani is the middle ground: a friendly browser editor over the real Free Pascal Compiler, with the rough edges (input handling, sandboxing, error UX) already filed down.
What you get out of the box:
- βοΈ CodeMirror editor with Pascal syntax highlighting, brackets, F5-to-run.
βΆοΈ Real compile + run againstfpc, including stdin support forreadlnβ type the input into a side panel and the program reads it.- π‘οΈ Hardened backend β Zod-validated requests, strict filename allowlist, per-IP rate limit, Helmet + CSP, per-request sandbox dirs.
- β‘ LRU compile cache β re-running the same code is instant.
- π§ͺ Real tests β Vitest + Supertest + Playwright (real Chromium against a real server). No tutorial markdown pretending to be tests.
- π¦ Docker image for teachers who don't want to install anything.
git clone <repo> && cd pholani
npm install
npm start # β http://localhost:3000Need the real compiler:
| OS | Command |
|---|---|
| macOS | brew install fpc |
| Debian / Ubuntu | sudo apt install fpc |
| Anywhere with Docker | docker build -t pholani . && docker run -p 3000:3000 pholani |
Don't have fpc and just want to poke around? npm test and npm run test:e2e both work β they use a bundled fake compiler.
The original Pholani couldn't run any program that called readln β the compiled binary had no stdin. Pholani 2 fixes that with a dedicated Program Input textarea: anything you type there is piped to the running program before it starts.
Module dependency graph:
Compile request β sequence:
Source for the diagrams lives at docs/diagrams/*.d2. Re-render with d2 file.d2 file.svg.
| Concern | Lives in | Notes |
|---|---|---|
| HTTP bootstrap | src/server.js |
Reads validated config, starts Express. |
| App composition | src/app.js |
Wires middleware + routes; no I/O. |
| Security headers / CORS | src/middleware/security.js |
Helmet + CSP, explicit origin allowlist. |
| Rate limiting | src/middleware/rateLimit.js |
In-memory LRU token bucket; per-IP. |
| Error mapping | src/middleware/errorHandler.js |
Maps err.statusCode / err.code to JSON. |
| Routes | src/routes/{compile,files,health}.js |
Thin β only Zod parsing + service calls. |
| Compile pipeline | src/services/compiler.js |
execFile for fpc, spawn for the produced binary with stdin piping. |
| File storage | src/services/fileStore.js |
Path-traversal-proof load/save into temp/uploads/. |
| In-memory caches | src/services/cache.js |
lru-cache instances. |
| Safe filenames / paths / hashing | src/lib/ |
Pure helpers, fully unit-tested. |
| Env validation | src/config/index.js |
zod schema β fail fast on misconfig. |
Key invariants:
- Per-request sandbox β every compile gets a fresh
temp/compile/<uuid>/directory, removed infinally. Concurrent students never collide. - Cache key = SHA-256 of
filename::code::stdinβ identical reruns return instantly without spawningfpc. spawn+ manual stdin pipe for the produced binary, with timeout and output-size caps.
| Cache | Key | Value | Size | TTL | Purpose |
|---|---|---|---|---|---|
compileCache |
sha256(filename::code::stdin) |
{output, error, compilationOutput} |
200 | 30 min | Skip recompilation for identical submissions (the common case in a classroom). |
fpcCheckCache |
installed |
{installed, version, message} |
1 | 5 min | Avoid spawning fpc -h on every page load. |
rateBuckets |
req:<ip> / compile:<ip> |
count | 10 000 | 1 min | Per-IP token bucket for general + compile traffic. |
All caches use lru-cache. They live in the Node process β no Redis or Memcached, no extra ops for a classroom deployment. Reset them in tests with resetAllCaches() from src/services/cache.js.
Pholani runs student-supplied code on the host. That is inherently risky. Here is what it defends against and what it does not.
Defends against:
| Threat | Defense | Where |
|---|---|---|
| Shell / command injection via filename | execFile (file form, never a shell) |
src/services/compiler.js |
Path traversal (e.g. ../../etc/passwd) |
Whitelist ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}\.pas$ + path.resolve containment |
src/lib/safeFilename.js, src/lib/safePath.js |
| Body-size DoS | express.json({ limit: '100kb' }) + Zod size limits |
src/app.js, src/routes/compile.js |
| Compile / run DoS | Per-IP token bucket (default 30 compiles / min, 120 requests / min) | src/middleware/rateLimit.js |
| Output-size DoS | Run stdout/stderr capped at 64 kB | src/services/compiler.js |
| Infinite loops | 10 s SIGKILL timeout |
src/services/compiler.js |
| Cross-origin abuse | Explicit CORS allowlist | src/middleware/security.js |
| Common header attacks | helmet with CSP |
src/middleware/security.js |
| Disclosure of host paths / stack traces | Sanitized JSON error responses | src/middleware/errorHandler.js |
Does not defend against:
- A compiled program calling
fpSystem/Exec/ making outbound network calls as the server user. Pascal programs run with full Unix privileges of the Node process. - A malicious user with shell access to the host.
- Filesystem exhaustion if you raise
PHOLANI_MAX_CODE_BYTESenormously and disable rate limiting.
Therefore: do not expose Pholani to the open internet without an OS-level sandbox. Recommended:
- Single-host classroom β run inside the bundled Docker image (
docker run -p 3000:3000 pholani). The container's filesystem is ephemeral. - Hardened β rootless container with
--read-only,--tmpfs /app/temp,--cpus/--memorycaps, auth reverse proxy. - Optional β replace
PHOLANI_FPC_BINwith a wrapper that invokes the compiler and produced binary insidensjail/firejail/bubblewrap.
All knobs are environment variables validated by src/config/index.js:
| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
HTTP listen port |
NODE_ENV |
development |
development | test | production |
PHOLANI_TEMP_DIR |
./temp |
Scratch dir for uploads + compile sandboxes |
PHOLANI_FPC_BIN |
fpc |
Path to the compiler (override for sandbox wrappers or tests) |
PHOLANI_CORS_ORIGINS |
http://localhost:3000 |
Comma-separated allowlist |
PHOLANI_MAX_CODE_BYTES |
100000 |
Hard ceiling on submitted source size |
PHOLANI_COMPILE_TIMEOUT_MS |
30000 |
Kill the compiler if it stalls |
PHOLANI_RUN_TIMEOUT_MS |
10000 |
Kill the produced binary if it loops |
PHOLANI_RUN_OUTPUT_BYTES |
65536 |
Clamp run stdout/stderr |
PHOLANI_RATE_COMPILE_PER_MIN |
30 |
Per-IP compile budget |
PHOLANI_RATE_REQUEST_PER_MIN |
120 |
Per-IP general budget |
Invalid values cause the server to crash on boot with a Zod error β by design.
| Command | What it does |
|---|---|
npm test |
Vitest unit + API tests with v8 coverage. Fails below 80% lines / functions / statements. |
npm run test:unit |
Just tests/unit/. |
npm run test:api |
Just tests/api/ (Supertest against an in-process Express app). |
npm run test:e2e |
Playwright: spins a real server on port 3456, drives Chromium, runs hello-world + readln + error scenarios. |
PHOLANI_RECORD_DEMOS=1 npm run test:e2e -- tests/e2e/demo.spec.js |
Re-records the browser demo gifs in docs/demos/. |
The bundled tests/fixtures/fake-fpc.sh lets the entire pipeline run on machines without Free Pascal installed.
.
βββ src/
β βββ server.js # bootstrap
β βββ app.js # composition root
β βββ config/index.js # Zod-validated env
β βββ middleware/{security,rateLimit,errorHandler}.js
β βββ routes/{compile,files,health}.js
β βββ services/{compiler,fileStore,cache}.js
β βββ lib/{safeFilename,safePath,sha}.js
βββ public/ # browser SPA (index.html, script.js, styles.css)
βββ tests/
β βββ unit/ # Vitest
β βββ api/ # Supertest
β βββ e2e/ # Playwright
β βββ fixtures/fake-fpc.sh
βββ docs/
β βββ diagrams/ # *.d2 source + *.svg rendered
β βββ demos/ # *.tape + *.gif
β βββ learning-examples/ # Pascal walkthroughs for students
β βββ pholani-overhaul-design.md # full v2 design spec
βββ Dockerfile
βββ docker-compose.yml
βββ .github/workflows/ci.yml
βββ CLAUDE.md
βββ CONTRIBUTING.md
βββ README.md
- CONTRIBUTING.md β dev setup, conventions, PR checklist.
- CLAUDE.md β house rules for AI / pair-programming sessions on this repo.
- docs/pholani-overhaul-design.md β full design spec for the v2 overhaul.
- docs/learning-examples/ β Pascal walkthroughs for Grade 12 students.
MIT.


