State and privacy
The state side of uwwoe is deliberately small. This page documents the shape, the privacy posture, and the operational guarantees.
Physically separate from the index
The writable state SQLite is at UWSCRAPE_STATE_DB_PATH (production:
/data/state.sqlite). It is a different file from
course-universe.sqlite. The runtime opens the state DB read-write in
WAL mode; the index DB is read-only. Mixing them is forbidden by
ADR 0010.
Tokens and verifiers
When you POST /api/v1/state, the backend:
- Generates ≥256 bits of cryptographic randomness.
- Base64url-encodes it (no padding).
- Computes an HMAC of the token under a rotatable server-side key
(
UWSCRAPE_TOKEN_KEY_PATH, materialised from a Fly secret at boot). - Stores the HMAC, the catalog version, and an initial empty state.
- Returns the raw token in the response body exactly once.
Every subsequent request carries the raw token as
Authorization: Bearer <token>. The backend recomputes the HMAC and
matches against the stored verifier — it cannot recover the token from
the verifier. Tokens never appear in URLs, logs, telemetry, worker
payloads, or test artifacts. (ADR 0011)
What survives a delete
DELETE /api/v1/state/current with explicit confirmation is a hard
delete:
- The state row is removed.
- The HMAC verifier is removed.
- A minimal tombstone is recorded — enough to make re-creation under the same token impossible, but containing no academic state contents (no terms, no courses, no grades, no notes).
- The same token, replayed, returns
401 unauthorized— indistinguishable from any other unknown token.
There is no soft delete. There is no 30-day grace period. There is no admin restore path. (ADR 0019)
Export
GET /api/v1/state/current/export returns a self-contained JSON copy
of the state with Cache-Control: no-store. It does not write a
state-event row. You can re-import the export later via
PUT /api/v1/state/current.
Migration
Plans are pinned to catalog_version_id. When a new index ships,
existing plans stay on their old catalog version until you explicitly
migrate:
POST /api/v1/state/current/migration-previewshows the diff.- A subsequent
PUT /api/v1/state/currentwith the migrated state applies it.
Migration is advisory and explicit. (ADR 0004)
Optimistic concurrency
Each state row has an expected_state_version counter. Mutating
requests include the expected value; mismatches return 409 Conflict
so a parallel edit elsewhere doesn’t silently overwrite.
This matters because the same token can be open in two tabs, two devices, or shared with a friend.
Concurrency under load
Mutations serialize through a WriteCoordinator queue — a single
in-process writer goroutine drains a bounded channel of submit
callbacks. Reads run in parallel on the SQLite WAL. The queue depth
and busy timeout are tunable; rate-limiting at the per-IP layer caps
abuse. (ADR 0027)
What logs and metrics contain
- Access logs (slog, structured): method, path, status, latency, size. Never the raw token, the state payload, grades, or notes.
- Metrics (
/internal/metrics, internal-bearer-gated): counts of state mutations, queue depth, evaluator latencies. No PII because there is no PII to leak.
What it cannot remember
- Who you are. There is no name, email, or student-number field.
- What you took last semester unless you put it on the Kanban.
- What your friend asked Advisory yesterday.
- Your earlier token after a hard delete.
Want the full spec?
- Backend state storage spec — schema, lifecycle, snapshot policy.
- Student state schema spec — JSON shape of
student_state. - Decision: Anonymous state token policy.
- Decision: V1 state deletion + export semantics.
- Decision: State DB concurrency model.