Skip to content

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:

  1. Generates ≥256 bits of cryptographic randomness.
  2. Base64url-encodes it (no padding).
  3. Computes an HMAC of the token under a rotatable server-side key (UWSCRAPE_TOKEN_KEY_PATH, materialised from a Fly secret at boot).
  4. Stores the HMAC, the catalog version, and an initial empty state.
  5. 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-preview shows the diff.
  • A subsequent PUT /api/v1/state/current with 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?