Appearance
Database Schema (Firestore)
The schema is documented where it's enforced — firestore-rules/firestore.rules carries a comment block per collection, and this page is its readable form. The model in one sentence: almost everything is per-user and owner-only; exactly two collections are shared, each with an explicitly documented reason and threat model.
rendering diagram…
drag to pan · ctrl / ⌘ + scroll to zoom
Per-user collections
| Path | Written by | Notes |
|---|---|---|
users/{uid} | AuthContext | email is synced on every sign-in — the fix for the reminder-skipping bug (Scheduling) |
…/events, …/tasks, …/notifications | useEvents/useTasks/useNotifications | events carry reminder windows + remindersSent |
…/submissions | runner + Worker | doubles as the judging queue: docs tagged oaAttemptId are claimed by the Worker via updateTime precondition |
…/codeDrafts/{section__slug} | useCodeDraft | one doc per problem, per-language map — localStorage is the fast tier, this is the durable one |
…/annotations/{docId} | useAnnotations | one doc per article/tab, items[] of anchored highlights |
…/oaTemplates | useOATemplates | public ones mirror to publicOAs |
…/revisions/{section__slug} | useRevisions | spaced-repetition schedule, history, counts |
…/oaAttempts | useOAAttempt | timer anchor startedAt (server timestamp = source of truth), anti-cheat counters, final analytics |
progress/{uid} sits top-level (not a subcollection) for historical reasons; same owner-only rule.
The two shared collections — and their honest limits
problemStats/{section}:{slug}:{language} — the cross-user histogram behind "Beats X% of accepted solutions." Any signed-in user can read (compute their percentile) and write (add their accepted run to a 10ms/10MB bucket via increment()). The rules file states the limitation outright: Firestore rules can't verify a write only increments rather than overwrites — enforcing that needs a trusted backend, which this stack doesn't have a slot for. Accepted deliberately because the stat is low-stakes; the comment includes the escalation path (move increments server-side) if it ever backs something that matters. Also deliberately not real-time — read once per result view, no listeners, keeping read costs flat.
publicOAs/{templateId} — world-readable published OA templates. Writes require sourceUid == request.auth.uid on create and on the existing doc for update/delete, so only the author touches their mirror. Attempts against a public template still live under the attempter's own users/{uid} — reads never expose anyone's private data.
Access patterns worth knowing
- Live (
onSnapshot): submissions (app-wide viaSubmissionsContext), the active OA attempt, calendar. One-shot: problemStats, question content (which isn't in Firestore at all — see Question Bank). - Server writers (the two Workers) bypass rules via service-account credentials over the REST API — their discipline is preconditions and claims, not rules (Workers).
undefinedis not a value. Firestore rejects documents containingundefined; every write path strips/defaults optional fields. This was a production bug in OA attempt writes, now a codebase-wide rule.
storage.rules covers the one Storage use (avatar uploads) with the same owner-only shape; avatars themselves are served via Cloudinary.