Firestore /users/{uid} — Data Dictionary

Ground truth for every field synced to Firestore, traced directly from the site’s source (not from Sheets output — the Sheets output is downstream of this and has had bugs). Two separate data sources feed the “Users (Auth + Firestore)” dashboard:

[!note] Local vs. cloud Every w_* key is gated behind sign-in — nothing is written to Firestore for a signed-out visitor. Sign-in pulls the cloud copy down and overwrites local storage; sign-out wipes local storage. There is never a merge step.


Auth fields (Identity Toolkit, not Firestore)

Field Type Description Notes
uid string Unique ID for this user, assigned by Firebase Auth doubles as the doc ID in /users/{uid}
email string The email address they signed in with  
providers string (joined) Which sign-in method(s) they’ve used comma-joined list of providerIds (e.g. google.com)
createdAt date When the account was first created from createdAt (ms since epoch)
lastLoginAt date Most recent sign-in from lastLoginAt (ms since epoch)
disabled boolean Whether an admin has blocked this account from signing in  

Firestore fields — SYNCED_KEYS (arrays of strings)

Defined in cloud-sync.js:22. Each is mirrored to localStorage while signed in, and pushed to Firestore via a PATCH with updateMask.fieldPaths scoped to that one key — so writes to one key never touch the others. On sign-in, the whole array is pulled and overwrites local.

Field Description Element type Written by Mutation Order semantics
w_favorites Blog posts the user has bookmarked/saved to read later post URL (string) _layouts/post.html (save/unsave button) push to add, splice to remove — can shrink append order; index 0 = oldest still-saved favorite (UI reverses it to show newest-first, see reading-list.js:51)
w_read Every blog post the user has opened, logged automatically post URL (string) _layouts/post.html (fires passively on every page view, signed-in only) push only, de-duped (indexOf check first) — never removed append order; index 0 = first ever post read, not most recent
w_completed Blog posts the user explicitly marked as finished reading post URL (string) _layouts/post.html (“Mark as complete” button) push only, de-duped — never removed append order; index 0 = first post marked complete
w_course_progress Every course lesson/lab/quiz the user has completed lesson/lab/quiz URL (string) _layouts/course-lesson.html, _layouts/course-lab.html, _includes/course-quiz-block.html push only, de-duped — never removed append order = completion order across the whole curriculum, not grouped by course
w_course_celebrated Which courses’ completion animation has already played, so it doesn’t replay on the next visit course id (string) courses/index.html push to add; explicitly removeItem‘d entirely in one dev/reset path (courses/index.html:376) append order

[!warning] Not currently in the dashboard w_course_celebrated exists in Firestore and is actively synced, but the Apps Script dashboard doesn’t read it yet — it’s UI bookkeeping (has the celebration animation played?) rather than a metric, but flagging it here in case you want it added.

[!warning] Order correction An earlier version of the dashboard script/notes assumed w_read’s order was most-recent-first. Source confirms it’s push()oldest-first. order: 0 in the Content Engagement sheet is the first thing read/completed/favorited, not the latest.


Firestore fields — MEMORY_JSON_KEYS (single JSON-stringified objects, LogicGate only)

Defined in cloud-sync.js:31. Unlike the arrays above, these never touch localStorage while signed in — they live only in memory and in Firestore, so a signed-in player’s scoring leaves nothing on a shared device. Each is written as one Firestore stringValue containing JSON.stringify(obj), not mapped field-by-field. Source: assets/js/logicgate.js.

w_logicgate_state — today’s game

Key Description Type Notes
day Which daily puzzle this game belongs to integer puzzle day index (dayIndex()); state resets to a fresh game whenever day no longer matches today’s index
guesses The words the user has guessed so far today, with the letter-by-letter result for each array of { word: string, result: string[] } one entry per guess submitted; result is per-letter ("correct" \| "present" \| "absent")
status Whether today’s game is still in progress or finished string "playing" \| "won" \| "lost"
finishRecorded Whether this finished game has already been counted into the all-time stats boolean guards against double-counting a finished game into w_logicgate_stats
hintUsed Whether the user used a hint on today’s puzzle boolean  

w_logicgate_stats — all-time stats

Key Description Type Notes
played Total games played, ever integer  
wins Total games won, ever integer  
maxStreak Longest daily-win streak ever reached integer  
distribution How many wins took 1 guess, 2 guesses, etc. integer[6] count of wins by guess-count, index 0 = won in 1 guess … index 5 = won in 6 guesses

w_logicgate_streak — daily win streak

Key Description Type Notes
date The date of the user’s last counted win string (YYYY-MM-DD) date of the last counted win
count Current consecutive-day win streak integer consecutive daily wins; same-day replay doesn’t change it, a gap of >1 day resets to 1

[!warning] w_mldle_* fields do not exist in this codebase Earlier sample data pasted into this conversation included w_mldle_state, w_mldle_stats, w_mldle_streak. A repo-wide search (grep -ri mldle) finds zero matches anywhere in the site’s HTML/JS. These fields aren’t written by any current code path — likely leftover/test data in Firestore from an earlier prototype, not a live feature. The dashboard script no longer reads them (removed per request), but if real user docs still contain this data in Firestore, it’s orphaned, not wrong.


Firestore fields — MEMORY_INT_KEYS (single integer, LogicGate only)

Defined in cloud-sync.js:37 (added 2026-07-05). Same “never touches localStorage while signed in” treatment as MEMORY_JSON_KEYS above, but written as a plain Firestore integerValue — no JSON wrapping, since there’s only one number.

Field Description Type Notes
w_logicgate_rank The player’s permanent LogicGate rank: their position, in first-ever-win order across every player, the first time they ever won a puzzle integer claimed once via CloudSync.claimRank(), never recalculated afterwards — absent/null means the player hasn’t won a puzzle yet (shown as “Unranked” in the UI)

[!note] Backed by a counter doc outside /users/{uid} The next rank to hand out is tracked on a single shared document, meta/logicgate_rank_counter (see firestore.rules) — not part of any user’s doc, so it won’t appear in a per-user Firestore export and doesn’t belong in a per-user dashboard sheet. Only the result of claiming a rank (the integer above) is stored per-user.


Fields synced to localStorage only — never in Firestore

Defined in cloud-sync.js:36 (LOCAL_ONLY_CLEAR_KEYS). Wiped on sign-out for shared-device privacy, same as the synced keys, but not present in the Firestore doc at all — don’t expect these in any Firestore export.

Field Description Notes
w_streak The user’s consecutive-day reading streak (separate from w_logicgate_streak) considered fine to be per-device
w_streak_celebrated Whether the reading-streak celebration animation has already played UI flag

Dashboard sheet ↔ field mapping (current script)

Sheet Source fields
Users all auth fields + counts of w_completed/w_course_progress/w_read + all of w_logicgate_state/stats/streak
Content Engagement w_completed, w_read (long format, type + url + order)
Favorites w_favorites (long format, own sheet — kept separate from Content Engagement)
Course Progress w_course_progress, url split into course/section by path segment
Game Distributions w_logicgate_stats.distribution (long format)
(not yet mapped) w_course_celebrated

[!note] Dashboard script now saved in-repo A copy of the actual Apps Script (syncFirestoreToSheet and friends) is saved at notes/firestore-dashboard-sync.gs — bound to the Google Sheet itself (script.google.com), not deployed from this repo, so it must still be pasted in manually after edits here. w_logicgate_rank was added to its Users sheet (column logicgate_rank) on 2026-07-05.