[{"data":1,"prerenderedAt":328},["ShallowReactive",2],{"content-\u002Fdocs\u002Frecipes\u002Fpersistence-policy":3},{"id":4,"title":5,"body":6,"description":16,"extension":322,"meta":323,"navigation":67,"path":324,"seo":325,"stem":326,"__hash__":327},"docs\u002Fdocs\u002Frecipes\u002Fpersistence-policy.md","Persistence policy",{"type":7,"value":8,"toc":315},"minimark",[9,13,17,22,25,147,154,161,168,172,179,189,192,196,199,207,218,233,236,239,246,250,283,287,311],[10,11,5],"h1",{"id":12},"persistence-policy",[14,15,16],"p",{},"What gets stored in a persisted draft, how schema changes\ninvalidate old payloads, and what persistence is — and isn't —\ndesigned for.",[18,19,21],"h2",{"id":20},"sparse-payloads","Sparse payloads",[14,23,24],{},"The persisted payload contains only opted-in paths:",[26,27,32],"pre",{"className":28,"code":29,"language":30,"meta":31,"style":31},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F Schema: { email: string, phone: string, cvv: string }\n\u002F\u002F register('email', { persist: true })\n\u002F\u002F register('phone', { persist: true })\n\u002F\u002F register('cvv')                     ← no opt-in\n\n\u002F\u002F Persisted payload, written under key attaform:signup:${fingerprint}\n{\n  v: 4,                                          \u002F\u002F attaform-internal envelope version\n  data: { form: { email: '…', phone: '…' } }     \u002F\u002F no `cvv`\n}\n","ts","",[33,34,35,44,50,56,62,69,75,82,102,141],"code",{"__ignoreMap":31},[36,37,40],"span",{"class":38,"line":39},"line",1,[36,41,43],{"class":42},"sJ8bj","\u002F\u002F Schema: { email: string, phone: string, cvv: string }\n",[36,45,47],{"class":38,"line":46},2,[36,48,49],{"class":42},"\u002F\u002F register('email', { persist: true })\n",[36,51,53],{"class":38,"line":52},3,[36,54,55],{"class":42},"\u002F\u002F register('phone', { persist: true })\n",[36,57,59],{"class":38,"line":58},4,[36,60,61],{"class":42},"\u002F\u002F register('cvv')                     ← no opt-in\n",[36,63,65],{"class":38,"line":64},5,[36,66,68],{"emptyLinePlaceholder":67},true,"\n",[36,70,72],{"class":38,"line":71},6,[36,73,74],{"class":42},"\u002F\u002F Persisted payload, written under key attaform:signup:${fingerprint}\n",[36,76,78],{"class":38,"line":77},7,[36,79,81],{"class":80},"sVt8B","{\n",[36,83,85,89,92,96,99],{"class":38,"line":84},8,[36,86,88],{"class":87},"sScJk","  v",[36,90,91],{"class":80},": ",[36,93,95],{"class":94},"sj4cs","4",[36,97,98],{"class":80},",                                          ",[36,100,101],{"class":42},"\u002F\u002F attaform-internal envelope version\n",[36,103,105,108,111,114,116,119,121,125,128,131,133,135,138],{"class":38,"line":104},9,[36,106,107],{"class":87},"  data",[36,109,110],{"class":80},": { ",[36,112,113],{"class":87},"form",[36,115,110],{"class":80},[36,117,118],{"class":87},"email",[36,120,91],{"class":80},[36,122,124],{"class":123},"sZZnC","'…'",[36,126,127],{"class":80},", ",[36,129,130],{"class":87},"phone",[36,132,91],{"class":80},[36,134,124],{"class":123},[36,136,137],{"class":80}," } }     ",[36,139,140],{"class":42},"\u002F\u002F no `cvv`\n",[36,142,144],{"class":38,"line":143},10,[36,145,146],{"class":80},"}\n",[14,148,149,150,153],{},"The ",[33,151,152],{},"v"," field on the envelope is internal to attaform — it tracks the\non-disk format and is bumped only when attaform itself changes the\nserialised shape. Consumers don't (and now can't) set it. Drafts\nsaved against a stale envelope version are dropped with a one-time\ndev-warn on read.",[14,155,156,157,160],{},"The envelope also round-trips the form's ",[33,158,159],{},"blankPaths"," set when\npopulated, so a numeric field cleared by the user stays visually\nempty after reload (storage holds the slim default; the\ndisplayed-empty state survives).",[14,162,163,164,167],{},"On hydration, opted-in fields restore from storage; non-opted fields\ncome from schema defaults. The opt-in set can change between mounts\n— a previously-persisted path that's no longer opted in stays in\nstorage until the next write (which won't include it) or an explicit\n",[33,165,166],{},"form.clearPersistedDraft(path)",".",[18,169,171],{"id":170},"including-errors","Including errors",[14,173,174,175,178],{},"Default ",[33,176,177],{},"include: 'form'"," persists just the values. Server-side\nvalidation errors on reload are usually stale and confusing.",[14,180,181,182,185,186,167],{},"For multi-step wizards where reconstructing errors is expensive,\n",[33,183,184],{},"include: 'form+errors'"," persists and re-hydrates ",[33,187,188],{},"errors",[14,190,191],{},"Errors on non-opted-in paths are dropped from the persisted envelope\n— a persisted error without a persisted value would dangle on\nrehydration.",[18,193,195],{"id":194},"auto-invalidation-on-schema-change","Auto-invalidation on schema change",[14,197,198],{},"Storage keys carry the schema's structural fingerprint:",[26,200,205],{"className":201,"code":203,"language":204,"meta":31},[202],"language-text","attaform:signup:7c3a0b   ← key on disk\n                       └────┘\n                       fingerprint of the current schema\n","text",[33,206,203],{"__ignoreMap":31},[14,208,209,210,213,214,217],{},"When the schema changes shape — adding \u002F removing \u002F renaming a\nfield, changing a leaf type, restructuring nested objects — the\nfingerprint changes. New writes go to a new key\n(",[33,211,212],{},"attaform:signup:9d2b1f","); the old key\n(",[33,215,216],{},"attaform:signup:7c3a0b",") becomes unreachable.",[14,219,220,221,224,225,228,229,232],{},"On the next mount, the orphan-cleanup pass enumerates keys under\n",[33,222,223],{},"attaform:signup"," (via ",[33,226,227],{},"FormStorage.listKeys","), keeps the\ncurrent-fingerprint entry, and removes the rest. No manual ",[33,230,231],{},"version","\nbump, no possibility of forgetting it, no draft drops when only\nrefinement logic changed (refinements collapse to opaque sentinels\nin the fingerprint).",[14,234,235],{},"The same orphan pass also wipes pre-fingerprint legacy entries\nwritten by older library versions, so upgrades clean up cleanly on\nthe next mount.",[14,237,238],{},"Malformed-shape entries (corrupted JSON, attaform-internal envelope-version\nmismatch, anything that doesn't match the expected payload contract)\nare wiped on read. \"Truly absent\" entries (the key was never set)\nare a no-op — the wipe only fires when there's actually something to\nclean.",[14,240,241,242,245],{},"If you need to force-invalidate a draft without changing the schema\n(e.g. shipping an unrelated field-validation tweak that you want\nusers to retest from scratch), call ",[33,243,244],{},"form.clearPersistedDraft()"," at\nmount or wrap the schema in a thin no-op layer that perturbs the\nfingerprint. The library deliberately doesn't expose a\n\"force-version\" knob — most consumers don't need it, and the schema\nfingerprint already captures every legitimate \"shape changed\"\nsignal.",[18,247,249],{"id":248},"what-persistence-is-not-for","What persistence is NOT for",[251,252,253,261,267,273],"ul",{},[254,255,256,260],"li",{},[257,258,259],"strong",{},"Sensitive data."," Don't persist passwords, payment cards, SSNs,\ntokens, or anything else listed in the sensitive-name heuristic\nunless your storage adapter encrypts AND the encryption key isn't\nitself client-side derivable. The library throws at mount on\nobvious cases; the heuristic isn't exhaustive.",[254,262,263,266],{},[257,264,265],{},"Authoritative state."," Persistence is for draft UX, not for\nsource-of-truth data. The server still owns the canonical record.",[254,268,269,272],{},[257,270,271],{},"Cross-form coordination."," Each form persists independently.\nMultiple forms can share a key (and so a FormStore + a persistence\nentry), but they're still one form to the persistence layer.",[254,274,275,278,279,282],{},[257,276,277],{},"Schema migrations."," Schema changes auto-invalidate old payloads\nvia the fingerprint (the old key becomes unreachable and is swept\non the next mount). If you need to rename a field without losing\nstate, read the raw entry yourself before the schema change ships\nand massage it into the new shape before calling ",[33,280,281],{},"reset()",". The\nlibrary deliberately doesn't ship a renaming-aware migration\nhelper — schemas are the contract; renames are a write-once\ntransformation the consumer owns.",[18,284,286],{"id":285},"see-also","See also",[251,288,289,297,304],{},[254,290,291,296],{},[292,293,295],"a",{"href":294},"\u002Fdocs\u002Frecipes\u002Fpersistence","Persistence walkthrough"," — the basics",[254,298,299,303],{},[292,300,302],{"href":301},"\u002Fdocs\u002Frecipes\u002Fpersistence-backends","Persistence backends"," — picking and configuring storage",[254,305,306,310],{},[292,307,309],{"href":308},"\u002Fdocs\u002Frecipes\u002Fpersistence-edge-cases","Persistence edge cases"," — imperative APIs, gotchas",[312,313,314],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":31,"searchDepth":46,"depth":46,"links":316},[317,318,319,320,321],{"id":20,"depth":46,"text":21},{"id":170,"depth":46,"text":171},{"id":194,"depth":46,"text":195},{"id":248,"depth":46,"text":249},{"id":285,"depth":46,"text":286},"md",{},"\u002Fdocs\u002Frecipes\u002Fpersistence-policy",{"title":5,"description":16},"docs\u002Frecipes\u002Fpersistence-policy","MsoscVfwN58WIc0YJl9-vQ_Wvm6ahJPpCJ2xHyQsUpU",1777934136551]