Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

cedarplan

Cedarville course planner: a degree audit read in the student’s own browser, and a shared cache of the public section catalog.

Domain: cedarplan.dunkirk.sh · Port: 3008 · Runtime: bun · Repo: the-cedarville-app

What the server holds, and what it does not

One SQLite file of course sections. No accounts, because there is nothing here to attach one to: a student’s transcript is read by a browser extension, stays in that browser, and is only ever written to disk by a companion process the student runs on their own machine.

That is worth stating in a deployment note because it is the only reason this service needs no secret and no backup of anything personal. The database is public course data and is rebuildable by any signed-in student, so it is backed up to save the crawl rather than to protect the contents.

No secret

secretsFile is deliberately unset. The planner authenticates to nothing.

Environment

VariableValueWhy
APP_ORIGINhttps://cedarplan.dunkirk.shCompiled into the page and the extension manifest. An extension cannot be told at runtime which origins may ask it for a transcript, so the origin is a build input.
CATALOG_DB${dataDir}/data/catalog.sqliteWAL mode, so restic copies it hot.
HOST127.0.0.1The app binds loopback by default; this is explicit rather than load-bearing.
CRAWLoffSee below.

Why CRAWL=off

The server used to crawl the course catalog itself, anonymously, from the public course search. Cedarville put that search behind SSO, so the only session that can read a timetable now is a signed-in student’s browser. The first student to open a term crawls it there and offers it to this cache; everybody after them reads the cache.

Left on, the server’s own crawl loop fails against a login page every thirty minutes and achieves nothing but log noise.

The one open write route

POST /catalog/<term>/ingest takes a crawl from whoever is signed in, because that is now the only way the cache can be filled. It is unauthenticated by design and guarded on the claim rather than the claimant:

  • every section validated field by field, one bad section refusing the batch
  • the crawl must say it reached the last page, or it may not replace a term
  • a crawl may not shrink a term by more than a fifth
  • every section must belong to the term it was posted under
  • the term must be a code the registrar could issue, in a plausible year

Caddy caps the body at 32MB, which clears a real term’s crawl with room to spare. A determined student can still post a plausible lie about the catalog; what they cannot do is delete it or grow it without bound.

The extension

The planner serves /install.html and cedarville-bridge.zip, built from the same source tree as the page. Chrome refuses to install an extension from a web page, so a student unzips it and loads it unpacked. The page asks the installed extension for its version at startup and says so when it is older than the planner, which is the failure that otherwise reads as the planner being broken.

Deploys

The app repo’s .github/workflows/deploy.yml calls the reusable workflow with no build_command, on purpose: bun start builds and then serves, so the build happens inside the unit where APP_ORIGIN is set. A build in the deploy workflow’s SSH shell would bake in localhost.