Skip to content

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.

diagram100%
rendering diagram…

drag to pan · ctrl / ⌘ + scroll to zoom

Per-user collections

PathWritten byNotes
users/{uid}AuthContextemail is synced on every sign-in — the fix for the reminder-skipping bug (Scheduling)
…/events, …/tasks, …/notificationsuseEvents/useTasks/useNotificationsevents carry reminder windows + remindersSent
…/submissionsrunner + Workerdoubles as the judging queue: docs tagged oaAttemptId are claimed by the Worker via updateTime precondition
…/codeDrafts/{section__slug}useCodeDraftone doc per problem, per-language map — localStorage is the fast tier, this is the durable one
…/annotations/{docId}useAnnotationsone doc per article/tab, items[] of anchored highlights
…/oaTemplatesuseOATemplatespublic ones mirror to publicOAs
…/revisions/{section__slug}useRevisionsspaced-repetition schedule, history, counts
…/oaAttemptsuseOAAttempttimer 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 via SubmissionsContext), 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).
  • undefined is not a value. Firestore rejects documents containing undefined; 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.

Ediky Workflow — internal engineering documentation.