Repository navigation
Deployment and Auth
Deployed to Fly.io as app classicalkenken (primary region ams), from the repo Dockerfile.
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 8080
CMD ["node", "server/index.js"]fly.toml:
-
[env]—DB_PATH=/data/scores.db,PORT=8080. -
[[mounts]]— a Fly volumekenken_datamounted at/dataholds the SQLite file (the only persistent state). -
[http_service]—force_https,auto_stop_machines/auto_start_machines(scales to zero),min_machines_running = 0. -
[[vm]]— 256 MB, 1 shared CPU.
.dockerignore keeps the build context lean and free of data: it excludes .git, data/ (local
databases, including any copy of production pulled for testing), *.db, the source question-bank
*.pdfs, and the SAT-Questions extraction scripts. Deploys usually run from a local checkout, so
without it COPY . . would bake whatever is on disk into the image.
Deploy: fly deploy. The single machine holds the DB on its volume, so there is effectively one
writer; don't scale to >1 machine without moving off local SQLite.
Schema and data migrations run automatically at startup in server/db.js — there is no manual
step between deploy and the migration touching production. Before a deploy that carries a data
migration:
- Snapshot the volume:
fly volumes list, thenfly volumes snapshots create <volume-id>. - Pull a consistent copy (the DB isn't in WAL mode, but a copy mid-write can still be torn):
fly ssh console -a classicalkenken node -e "new (require('node:sqlite').DatabaseSync)('/data/scores.db').exec(\"VACUUM INTO '/data/scores-copy.db'\")" exit fly ssh sftp get /data/scores-copy.db data/prod-scores.db -a classicalkenken fly ssh console -a classicalkenken -C "rm /data/scores-copy.db"
- Rehearse against the copy (e.g.
node tools/preview-english-migration.js data/prod-scores.db, or boot the server withDB_PATHpointed at a scratch copy of it). Delete the copy afterwards — it holds student data.
Domains: classicaltech.org (primary), classicalkenken-g0z3ra.fly.dev (Fly default). The
it.<host> subdomain is rewritten in server/index.js to serve the /it bundle.
| Var | Purpose | Default |
|---|---|---|
PORT |
listen port | 8080 |
DB_PATH |
SQLite file path | <repo>/data/scores.db |
DEV_USER |
bypass requireAuth — every student route acts as this user_key
|
unset |
DEV_TEACHER |
bypass requireTeacher — every teacher route acts as this teacher_key
|
unset |
Local run:
npm ci
DB_PATH=/tmp/dev.db PORT=8090 DEV_TEACHER=devteacher DEV_USER=devstudent node server/index.js-
Client ID (public, in
js/nav.jsandjs/auth.js):245572615958-jg9jin9k68eh70pk659ifhjc8unbopta.apps.googleusercontent.com -
Scope:
https://www.googleapis.com/auth/userinfo.emailonly. No sensitive scopes. -
Flow: GIS implicit token flow (
google.accounts.oauth2.initTokenClient). The browser receives a short-lived Google access token; there is no client secret in the front-end.
To move the deployment to a new domain, add it to the OAuth client's Authorized JavaScript
origins in Google Cloud Console and update CLIENT_ID if a new client is created.
Both server/auth.js (students) and server/teacherAuth.js (teachers):
- Look up the bearer token in
sessions/teacher_sessionsfirst — a hit means an already-issued platform session, no network call. - Otherwise treat it as a raw Google access token:
GET https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=…, checkemailand domain.- Students:
@students.hartfordschools.orgor@hartfordschools.org. - Teachers:
@hartfordschools.orgonly (student subdomain explicitly excluded).
- Students:
-
user_key/teacher_key= the email local part.usersgets anINSERT OR IGNORE. - Verified tokens are cached in-memory until their
expires_in.
POST /api/auth/login / POST /api/teacher/login exchange a valid Google token for a random UUID
session token, stored in sessions / teacher_sessions and in the browser's localStorage
(classtech_session / classtech_teacher_session). These don't expire server-side; sign-out deletes
the row and the localStorage key. The teacher session token is also what the DobbsCore extension
uses (Copy Token).
Any @hartfordschools.org account that signs in becomes a teacher with its own empty space. Any
@students.hartfordschools.org account becomes a student. Provisioning is implicit.
Only the email address and practice scores are stored, in the SQLite file on the Fly volume, keyed to
the email. The canonical policy is privacy-policy.md in the repo and the "Privacy Policy" section of
the on-site ReadMe page (readme/index.html). Terms: terms-of-service.md.
The old wiki pages Google-Cloud-Services, Spreadsheet-Integration, and Code described a pre-fork version that wrote scores to a Google Sheet and used Gmail scopes. None of that is true anymore — this page supersedes them.