Where Does Your Work Live?
A user makes a change in the editor. If their browser crashes, their session expires, they switch devices, they switch from parent to child — what survives, where?
§1 The big idea
FuzzyCode has no autosave. A user's in-progress work lives in four layers with very different lifetimes, scopes, and recovery paths. Understanding which layer holds what is the difference between "I lost my work" and "my work is fine, I just need to refresh."
§2 The four storage layers
1. projectVersionHistory · in-memory JS global per-tab
{ html, prompt, diff, provenance, pii_attestation } — the editor's authoritative viewsaveVersion() — only on user action (send, swipe, paste, code edit, metadata, etc.)FuzzyCode/static/script.js:28652. sessionStorage · fast restore per-tab, per-session
projectVersionHistory, input values, last-selected versionwindow_tab_saveState() — after every saveVersion + on unloadwindow_tab_state_restorer on page loadprojectId + user/child context — household switches don't collidewindow_tab_state_restorer.js:255-3193. IndexedDB · fuzzycode_recent_tabs / tab_state per-browser, per-child
fuzzycode_recent_tabs / tab_statechild:<childId>:project:<pid> — partitioned per childsaveVersion + on unload/local_recent_history popup with Active vs Legacy buckets (after context switches)FuzzyCode/static/fuzzy-blocks/tab_state_storage.js4. Redis · temp_page:* keys server, per-child
temp_page:<hash> — keyed by content hash_owns_temp_page gate/user_projects (cross-device list) · /project_history/<pid> (diff timeline) · iframe via usercontent.fuzzycode.dev Worker403 CHILD_CONTEXT_REQUIRED on /temp_page/*FuzzyCode/quart_server/blueprints/temp_pages.py:1815. Pages DB + S3 · published rows canonical, forever
pages Postgres row + screenshot in S3 (aws.fuzzycode.dev/uploaded/<hash>.webp)@pages/submit/delete_page)pages.fuzzycode.dev/page/<hash> public URL · share tokens for unlisted§3 Survival matrix — what survives what?
Columns = the four in-progress layers (Publish always survives once done). Rows = recovery events a user might experience. Read a row to see what happens to their work.
| Recovery event | MemoryprojectVersionHistory |
sessionStorage | IndexedDB (per-child) |
Redistemp_page:* |
|---|---|---|---|---|
| Tab refresh (same tab) | lost | restored → memory | unaffected | unaffected |
| Tab close, reopen later | lost | lost | user sees in /local_recent_history | visible in /user_projects (30d) |
| Browser close + reopen | lost | lost | survives | survives |
| Clear cookies | still in RAM if tab open | scoped-keys may survive | IndexedDB unaffected | 401 on /temp_page/* until re-login |
| Clear site data | lost | lost | lost | survives on server (30d) |
| JWT expires mid-edit | intact | intact | intact | next write → 401 until refresh |
| Switch to a different device | n/a | n/a | does not cross devices | /user_projects shows last 30d |
| Parent → Child context switch | cleared; new child's history loaded | key-scoped — doesn't collide | per-child key namespace; other child's records shown as "Legacy / unassigned" | server re-scopes by child_id |
| Publish succeeds | unaffected | unaffected | unaffected | new pages row takes over as canonical |
§4 The three user-facing "history" surfaces
Each uses a different backing store. Users need to understand which one they're looking at.
Local Recent History
/local_recent_historyShows every project the user has touched in this browser. Opens as a popup. Partitioned per-child — switching to another child shows their own list; prior child's projects appear as "Legacy / unassigned" bucket.
Doesn't cross devices.
My Projects
/user_projectsThe cross-device project list — whatever the user has touched on any device in the last 30 days. Redis-backed, so fast but server-filtered by child scope.
Any project the user hasn't touched in 30 days disappears from this list — but is still retrievable if it was published.
Project Version History
/project_history/<pid>A timeline view of all versions of a single project, with diffs. Used for review and audit, not for authoring.
§5 Fork / duplicate — what exists, what doesn't
There is no in-editor fork button. Users "duplicate" by loading a shared URL or a published Pages page, which creates a fresh projectVersionHistory[newProjectId] with referencePageID set to the source.
projectVersionHistory]:::action U -->|paste into editor clipboard| L2[
loadHtmlFromUrl orpaste_as_new_project]:::action L1 --> N[New project id minted
referencePageID = source]:::new L2 --> N N --> E[User iterates normally]:::new E -->|Publish with duplication_strategy=duplicate| DP[New Pages row]:::new E -->|Publish with duplication_strategy=update
AND hash matches| DP2[⚠ silent overwrite of original]:::action
update strategy can silently overwrite the original. If the user duplicated from an existing page, edited it, and chose "update" on publish — if the hash happens to still match, the original page row is replaced. No confirmation dialog on the first Publish that opts into update.
§6 Metadata page — what it persists
The "Metadata" button opens a popup that introspects the current HTML (via /get_metadata) and lets the user edit extracted fields (title, description, images, sounds, controls). Metadata has no separate store — edits produce a new version with lastAction=update_metadata, saved to projectVersionHistory like any other change.
/metadata_page participant S as Server
/get_metadata participant IF as Iframe U->>E: click puzzle icon #openMetadataBtn E->>MP: open popup MP->>S: GET /get_metadata (current HTML) S-->>MP: { title, description, images[], sounds[], controls[] } MP-->>U: editable form U->>MP: edits fields, clicks save MP->>E: postMessage metadata changes E->>E: saveVersion('update_metadata', merged HTML) E->>IF: re-render
No separate metadata DB. The source of truth for metadata is what's embedded in the page's HTML.
§7 Gotchas
saveVersion, it's gone./temp_page writes. /temp_page/* returns 403 CHILD_CONTEXT_REQUIRED. This also breaks iframe preview for unselected-parent sessions.referencePageID is mutated on past versions at publish time. This means a version created before publish may have referencePageID=null and, after publish, show the new page's ID. Diagnostics that assume this field is immutable will be misled.window_tab_inclusion_list gates which globals survive reload. Only whitelisted names are written to sessionStorage. New globals added to script.js need explicit inclusion.child:<childId>. Parent acting as parent vs acting as Child A vs acting as Child B all see separate project lists.§8 Verification pointers
projectVersionHistorydeclarationFuzzyCode/static/script.js:2865saveVersionscript.js:3227-3375- Tab-state restorer
FuzzyCode/static/fuzzy-blocks/window_tab_state_restorer.js:255-319 - IndexedDB storage
FuzzyCode/static/fuzzy-blocks/tab_state_storage.js - Local history popup
script.js:3763-3810 /local_recent_historyroute (served by usercontent Worker allowlist)/user_projectsserver routeFuzzyCode/quart_server/blueprints/temp_pages.py/temp_pagewritetemp_pages.py:181- Ownership check
temp_pages.py(_owns_temp_page) - Metadata popup
script.js:4950-5026 - UGC Worker allowlist
docs/cloudflare/workers/usercontent-proxy.js:27-36