Skip to content

Deployment and Auth

benjamindobbs edited this page Sep 28, 2026 · 2 revisions

Deployment & Auth

Hosting

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 volume kenken_data mounted at /data holds 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:

  1. Snapshot the volume: fly volumes list, then fly volumes snapshots create <volume-id>.
  2. 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"
  3. Rehearse against the copy (e.g. node tools/preview-english-migration.js data/prod-scores.db, or boot the server with DB_PATH pointed 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.

Environment variables

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

Authentication

Google OAuth client

  • Client ID (public, in js/nav.js and js/auth.js): 245572615958-jg9jin9k68eh70pk659ifhjc8unbopta.apps.googleusercontent.com
  • Scope: https://www.googleapis.com/auth/userinfo.email only. 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.

Server-side verification

Both server/auth.js (students) and server/teacherAuth.js (teachers):

  1. Look up the bearer token in sessions / teacher_sessions first — a hit means an already-issued platform session, no network call.
  2. Otherwise treat it as a raw Google access token: GET https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=…, check email and domain.
    • Students: @students.hartfordschools.org or @hartfordschools.org.
    • Teachers: @hartfordschools.org only (student subdomain explicitly excluded).
  3. user_key / teacher_key = the email local part. users gets an INSERT OR IGNORE.
  4. Verified tokens are cached in-memory until their expires_in.

Platform sessions

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).

There is no allow-list

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.

Privacy

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.

Clone this wiki locally