[{"data":1,"prerenderedAt":943},["ShallowReactive",2],{"content-\u002Fdocs\u002Frecipes\u002Fpersistence-backends":3},{"id":4,"title":5,"body":6,"description":936,"extension":937,"meta":938,"navigation":369,"path":939,"seo":940,"stem":941,"__hash__":942},"docs\u002Fdocs\u002Frecipes\u002Fpersistence-backends.md","Persistence backends",{"type":7,"value":8,"toc":924},"minimark",[9,13,22,27,124,139,145,149,278,305,309,336,387,404,426,434,453,460,464,467,831,845,861,865,871,884,888,891,895,920],[10,11,5],"h1",{"id":12},"persistence-backends",[14,15,16,17,21],"p",{},"How to pick a storage backend, configure operational options, and\nplug in a custom ",[18,19,20],"code",{},"FormStorage"," adapter.",[23,24,26],"h2",{"id":25},"picking-a-backend","Picking a backend",[28,29,30,49],"table",{},[31,32,33],"thead",{},[34,35,36,40,43,46],"tr",{},[37,38,39],"th",{},"Backend",[37,41,42],{},"Size budget",[37,44,45],{},"Sync\u002Fasync",[37,47,48],{},"Best for",[50,51,52,69,83,110],"tbody",{},[34,53,54,60,63,66],{},[55,56,57],"td",{},[18,58,59],{},"'local'",[55,61,62],{},"~5 MB",[55,64,65],{},"sync",[55,67,68],{},"Small forms, widest compatibility. Shared across same-origin tabs.",[34,70,71,76,78,80],{},[55,72,73],{},[18,74,75],{},"'session'",[55,77,62],{},[55,79,65],{},[55,81,82],{},"Tab-scoped scratch state. Closes with the tab.",[34,84,85,90,93,96],{},[55,86,87],{},[18,88,89],{},"'indexeddb'",[55,91,92],{},"50%+ of disk",[55,94,95],{},"async",[55,97,98,99,102,103,102,106,109],{},"Large forms. ",[18,100,101],{},"Date"," \u002F ",[18,104,105],{},"Map",[18,107,108],{},"Set"," \u002F typed arrays round-trip verbatim.",[34,111,112,116,119,121],{},[55,113,114],{},[18,115,20],{},[55,117,118],{},"You decide",[55,120,118],{},[55,122,123],{},"Encrypted stores, cookie-backed, native-mobile bridges.",[14,125,126,128,129,131,132,135,136,138],{},[18,127,59],{}," and ",[18,130,75],{}," go through ",[18,133,134],{},"JSON.stringify"," — non-JSON\nleaves lose fidelity. ",[18,137,89],{}," uses the browser's structured-\nclone algorithm, so those leaves round-trip cleanly.",[14,140,141,142,144],{},"Only the backend you choose is bundled. Pick ",[18,143,59],{},", don't pay\nfor the IndexedDB code.",[23,146,148],{"id":147},"full-options","Full options",[150,151,156],"pre",{"className":152,"code":153,"language":154,"meta":155,"style":155},"language-ts shiki shiki-themes github-light github-dark","persist: {\n  storage: 'local' | 'session' | 'indexeddb' | FormStorage,\n  key?: string,                     \u002F\u002F default: attaform:${formKey}\n                                    \u002F\u002F (the resolved storage key adds a :${fingerprint} suffix automatically)\n  debounceMs?: number,              \u002F\u002F default 300\n  include?: 'form' | 'form+errors', \u002F\u002F default 'form'\n  clearOnSubmitSuccess?: boolean,   \u002F\u002F default true\n}\n","ts","",[18,157,158,171,200,216,222,236,258,272],{"__ignoreMap":155},[159,160,163,167],"span",{"class":161,"line":162},"line",1,[159,164,166],{"class":165},"sScJk","persist",[159,168,170],{"class":169},"sVt8B",": {\n",[159,172,174,177,180,183,187,190,192,195,197],{"class":161,"line":173},2,[159,175,176],{"class":165},"  storage",[159,178,179],{"class":169},": ",[159,181,59],{"class":182},"sZZnC",[159,184,186],{"class":185},"szBVR"," |",[159,188,189],{"class":182}," 'session'",[159,191,186],{"class":185},[159,193,194],{"class":182}," 'indexeddb'",[159,196,186],{"class":185},[159,198,199],{"class":169}," FormStorage,\n",[159,201,203,206,209,212],{"class":161,"line":202},3,[159,204,205],{"class":169},"  key",[159,207,208],{"class":185},"?:",[159,210,211],{"class":169}," string,                     ",[159,213,215],{"class":214},"sJ8bj","\u002F\u002F default: attaform:${formKey}\n",[159,217,219],{"class":161,"line":218},4,[159,220,221],{"class":214},"                                    \u002F\u002F (the resolved storage key adds a :${fingerprint} suffix automatically)\n",[159,223,225,228,230,233],{"class":161,"line":224},5,[159,226,227],{"class":169},"  debounceMs",[159,229,208],{"class":185},[159,231,232],{"class":169}," number,              ",[159,234,235],{"class":214},"\u002F\u002F default 300\n",[159,237,239,242,244,247,249,252,255],{"class":161,"line":238},6,[159,240,241],{"class":169},"  include",[159,243,208],{"class":185},[159,245,246],{"class":182}," 'form'",[159,248,186],{"class":185},[159,250,251],{"class":182}," 'form+errors'",[159,253,254],{"class":169},", ",[159,256,257],{"class":214},"\u002F\u002F default 'form'\n",[159,259,261,264,266,269],{"class":161,"line":260},7,[159,262,263],{"class":169},"  clearOnSubmitSuccess",[159,265,208],{"class":185},[159,267,268],{"class":169}," boolean,   ",[159,270,271],{"class":214},"\u002F\u002F default true\n",[159,273,275],{"class":161,"line":274},8,[159,276,277],{"class":169},"}\n",[14,279,280,281,284,285,288,289,292,293,296,297,300,301,304],{},"Note what's NOT here. There's no ",[18,282,283],{},"fields:"," allowlist, no ",[18,286,287],{},"paths:","\nallowlist, no ",[18,290,291],{},"redactFields:"," blocklist, and no ",[18,294,295],{},"version:"," knob.\nPersisted fields are announced at the ",[18,298,299],{},"register()"," call site —\nthat's the entire opt-in surface. Schema-change invalidation flows\nfrom the schema's fingerprint, not a manual version field. The\nform-level ",[18,302,303],{},"persist:"," config is operational only.",[23,306,308],{"id":307},"switching-backends-safely","Switching backends safely",[14,310,311,312,315,316,254,318,254,320,322,323,326,327,329,330,332,333,335],{},"The configured ",[18,313,314],{},"storage"," is the source of truth for \"where the draft\nlives now.\" On every mount, the orphan-cleanup pass scans the three\nstandard backends (",[18,317,59],{},[18,319,75],{},[18,321,89],{},") under the\nform's ",[18,324,325],{},"key"," prefix and removes anything that doesn't match the\nconfigured backend's current-fingerprint entry. So if a form was\npersisting to ",[18,328,59],{}," and you switch to ",[18,331,75],{}," (or to a custom\nencrypted adapter), the stale ",[18,334,59],{}," entry can't orphan PII or\nsensitive fields.",[150,337,339],{"className":152,"code":338,"language":154,"meta":155,"style":155},"\u002F\u002F Before:\nuseForm({ schema, key: 'signup', persist: 'local' })\n\n\u002F\u002F After (next deploy): mount-time sweep wipes the old 'local' entry.\nuseForm({ schema, key: 'signup', persist: encryptedStorage })\n",[18,340,341,346,365,371,376],{"__ignoreMap":155},[159,342,343],{"class":161,"line":162},[159,344,345],{"class":214},"\u002F\u002F Before:\n",[159,347,348,351,354,357,360,362],{"class":161,"line":173},[159,349,350],{"class":165},"useForm",[159,352,353],{"class":169},"({ schema, key: ",[159,355,356],{"class":182},"'signup'",[159,358,359],{"class":169},", persist: ",[159,361,59],{"class":182},[159,363,364],{"class":169}," })\n",[159,366,367],{"class":161,"line":202},[159,368,370],{"emptyLinePlaceholder":369},true,"\n",[159,372,373],{"class":161,"line":218},[159,374,375],{"class":214},"\u002F\u002F After (next deploy): mount-time sweep wipes the old 'local' entry.\n",[159,377,378,380,382,384],{"class":161,"line":224},[159,379,350],{"class":165},[159,381,353],{"class":169},[159,383,356],{"class":182},[159,385,386],{"class":169},", persist: encryptedStorage })\n",[14,388,389,390,393,394,399,400,403],{},"Custom adapters can't be enumerated by the runtime, but attaform still\ncalls each custom adapter's ",[18,391,392],{},"listKeys(prefix)"," for orphan-suffix\nsweeping on the configured backend itself (see\n",[395,396,398],"a",{"href":397},"\u002Fdocs\u002Frecipes\u002Fpersistence-policy#auto-invalidation-on-schema-change","Auto-invalidation on schema change",").\nAdapters that can't enumerate (HTTP-backed, cookie-backed) return\n",[18,401,402],{},"[]"," and the sweep degrades gracefully on those backends.\nConfiguring a custom adapter still sweeps all three standard\nbackends — the dev might have migrated away from any of them.",[14,405,406,407,409,410,413,414,417,418,421,422,425],{},"The cleanup runs once at mount, only touches the ",[18,408,325],{}," prefix your\nform resolves to (default ",[18,411,412],{},"attaform:${formKey}","), and never\ntouches keys outside that prefix. Entries other forms wrote to the\nsame backend under different keys are untouched. The exact-or-",[18,415,416],{},":","-\nprefix match prevents collision with sibling forms whose keys share\na string prefix (e.g. custom keys ",[18,419,420],{},"my-form"," vs ",[18,423,424],{},"my-form-2",").",[427,428,430,431,433],"h3",{"id":429},"removing-persist-entirely","Removing ",[18,432,303],{}," entirely",[14,435,436,437,439,440,443,444,446,447,449,450,452],{},"Removing the ",[18,438,303],{}," option from ",[18,441,442],{},"useForm()"," is the same hygiene\nproblem one step further. Attaform sweeps all three standard backends for\nthe form's default key whenever ",[18,445,442],{}," is called without a\n",[18,448,303],{}," option, so a deployment that disables persistence (for\ncompliance, simplification, whatever) actually clears the on-disk\nartifact instead of leaving a stale entry under\n",[18,451,412],{}," indefinitely.",[14,454,455,456,459],{},"Caveat: only the default key is reachable. If a previous deployment\nused a custom ",[18,457,458],{},"persist.key",", that's an explicit migration on the\nconsumer.",[23,461,463],{"id":462},"custom-backend","Custom backend",[14,465,466],{},"The escape hatch — implement the four-method contract and pass the\nobject directly:",[150,468,470],{"className":152,"code":469,"language":154,"meta":155,"style":155},"import type { FormStorage } from 'attaform'\n\nconst encryptedStorage: FormStorage = {\n  async getItem(key) {\n    const raw = await fetch(`\u002Fapi\u002Fdrafts\u002F${key}`).then((r) => r.json() as Promise\u003Cunknown>)\n    return raw\n  },\n  async setItem(key, value) {\n    await fetch(`\u002Fapi\u002Fdrafts\u002F${key}`, { method: 'PUT', body: JSON.stringify(value) })\n  },\n  async removeItem(key) {\n    await fetch(`\u002Fapi\u002Fdrafts\u002F${key}`, { method: 'DELETE' })\n  },\n  async listKeys(prefix) {\n    \u002F\u002F Used by the orphan-cleanup pass to find stale fingerprint-suffixed keys.\n    \u002F\u002F Return every key whose name starts with `prefix`. If your backend\n    \u002F\u002F can't enumerate (no list endpoint, opaque cookies), return [].\n    const r = await fetch(`\u002Fapi\u002Fdrafts?prefix=${encodeURIComponent(prefix)}`)\n    return (await r.json()) as string[]\n  },\n}\n\nuseForm({ schema, key: 'signup', persist: { storage: encryptedStorage } })\n",[18,471,472,489,493,513,530,597,605,610,628,665,670,684,706,711,726,732,738,744,778,804,809,814,819],{"__ignoreMap":155},[159,473,474,477,480,483,486],{"class":161,"line":162},[159,475,476],{"class":185},"import",[159,478,479],{"class":185}," type",[159,481,482],{"class":169}," { FormStorage } ",[159,484,485],{"class":185},"from",[159,487,488],{"class":182}," 'attaform'\n",[159,490,491],{"class":161,"line":173},[159,492,370],{"emptyLinePlaceholder":369},[159,494,495,498,502,504,507,510],{"class":161,"line":202},[159,496,497],{"class":185},"const",[159,499,501],{"class":500},"sj4cs"," encryptedStorage",[159,503,416],{"class":185},[159,505,506],{"class":165}," FormStorage",[159,508,509],{"class":185}," =",[159,511,512],{"class":169}," {\n",[159,514,515,518,521,524,527],{"class":161,"line":218},[159,516,517],{"class":185},"  async",[159,519,520],{"class":165}," getItem",[159,522,523],{"class":169},"(",[159,525,325],{"class":526},"s4XuR",[159,528,529],{"class":169},") {\n",[159,531,532,535,538,540,543,546,548,551,553,556,558,561,564,567,570,573,576,579,582,585,588,591,594],{"class":161,"line":224},[159,533,534],{"class":185},"    const",[159,536,537],{"class":500}," raw",[159,539,509],{"class":185},[159,541,542],{"class":185}," await",[159,544,545],{"class":165}," fetch",[159,547,523],{"class":169},[159,549,550],{"class":182},"`\u002Fapi\u002Fdrafts\u002F${",[159,552,325],{"class":169},[159,554,555],{"class":182},"}`",[159,557,425],{"class":169},[159,559,560],{"class":165},"then",[159,562,563],{"class":169},"((",[159,565,566],{"class":526},"r",[159,568,569],{"class":169},") ",[159,571,572],{"class":185},"=>",[159,574,575],{"class":169}," r.",[159,577,578],{"class":165},"json",[159,580,581],{"class":169},"() ",[159,583,584],{"class":185},"as",[159,586,587],{"class":165}," Promise",[159,589,590],{"class":169},"\u003C",[159,592,593],{"class":500},"unknown",[159,595,596],{"class":169},">)\n",[159,598,599,602],{"class":161,"line":238},[159,600,601],{"class":185},"    return",[159,603,604],{"class":169}," raw\n",[159,606,607],{"class":161,"line":260},[159,608,609],{"class":169},"  },\n",[159,611,612,614,617,619,621,623,626],{"class":161,"line":274},[159,613,517],{"class":185},[159,615,616],{"class":165}," setItem",[159,618,523],{"class":169},[159,620,325],{"class":526},[159,622,254],{"class":169},[159,624,625],{"class":526},"value",[159,627,529],{"class":169},[159,629,631,634,636,638,640,642,644,647,650,653,656,659,662],{"class":161,"line":630},9,[159,632,633],{"class":185},"    await",[159,635,545],{"class":165},[159,637,523],{"class":169},[159,639,550],{"class":182},[159,641,325],{"class":169},[159,643,555],{"class":182},[159,645,646],{"class":169},", { method: ",[159,648,649],{"class":182},"'PUT'",[159,651,652],{"class":169},", body: ",[159,654,655],{"class":500},"JSON",[159,657,658],{"class":169},".",[159,660,661],{"class":165},"stringify",[159,663,664],{"class":169},"(value) })\n",[159,666,668],{"class":161,"line":667},10,[159,669,609],{"class":169},[159,671,673,675,678,680,682],{"class":161,"line":672},11,[159,674,517],{"class":185},[159,676,677],{"class":165}," removeItem",[159,679,523],{"class":169},[159,681,325],{"class":526},[159,683,529],{"class":169},[159,685,687,689,691,693,695,697,699,701,704],{"class":161,"line":686},12,[159,688,633],{"class":185},[159,690,545],{"class":165},[159,692,523],{"class":169},[159,694,550],{"class":182},[159,696,325],{"class":169},[159,698,555],{"class":182},[159,700,646],{"class":169},[159,702,703],{"class":182},"'DELETE'",[159,705,364],{"class":169},[159,707,709],{"class":161,"line":708},13,[159,710,609],{"class":169},[159,712,714,716,719,721,724],{"class":161,"line":713},14,[159,715,517],{"class":185},[159,717,718],{"class":165}," listKeys",[159,720,523],{"class":169},[159,722,723],{"class":526},"prefix",[159,725,529],{"class":169},[159,727,729],{"class":161,"line":728},15,[159,730,731],{"class":214},"    \u002F\u002F Used by the orphan-cleanup pass to find stale fingerprint-suffixed keys.\n",[159,733,735],{"class":161,"line":734},16,[159,736,737],{"class":214},"    \u002F\u002F Return every key whose name starts with `prefix`. If your backend\n",[159,739,741],{"class":161,"line":740},17,[159,742,743],{"class":214},"    \u002F\u002F can't enumerate (no list endpoint, opaque cookies), return [].\n",[159,745,747,749,752,754,756,758,760,763,766,768,770,773,775],{"class":161,"line":746},18,[159,748,534],{"class":185},[159,750,751],{"class":500}," r",[159,753,509],{"class":185},[159,755,542],{"class":185},[159,757,545],{"class":165},[159,759,523],{"class":169},[159,761,762],{"class":182},"`\u002Fapi\u002Fdrafts?prefix=${",[159,764,765],{"class":165},"encodeURIComponent",[159,767,523],{"class":182},[159,769,723],{"class":169},[159,771,772],{"class":182},")",[159,774,555],{"class":182},[159,776,777],{"class":169},")\n",[159,779,781,783,786,789,791,793,796,798,801],{"class":161,"line":780},19,[159,782,601],{"class":185},[159,784,785],{"class":169}," (",[159,787,788],{"class":185},"await",[159,790,575],{"class":169},[159,792,578],{"class":165},[159,794,795],{"class":169},"()) ",[159,797,584],{"class":185},[159,799,800],{"class":500}," string",[159,802,803],{"class":169},"[]\n",[159,805,807],{"class":161,"line":806},20,[159,808,609],{"class":169},[159,810,812],{"class":161,"line":811},21,[159,813,277],{"class":169},[159,815,817],{"class":161,"line":816},22,[159,818,370],{"emptyLinePlaceholder":369},[159,820,822,824,826,828],{"class":161,"line":821},23,[159,823,350],{"class":165},[159,825,353],{"class":169},[159,827,356],{"class":182},[159,829,830],{"class":169},", persist: { storage: encryptedStorage } })\n",[14,832,833,834,837,838,840,841,844],{},"All four methods are Promise-returning so sync and async backends\nshare one shape. ",[18,835,836],{},"getItem"," returns ",[18,839,593],{}," so your backend can\nhand back whatever ",[18,842,843],{},"setItem"," received.",[14,846,847,849,850,853,854,856,857,860],{},[18,848,392],{}," is what powers schema-change auto-invalidation:\nwhen the schema's fingerprint changes, the orphan cleanup pass\nenumerates keys under the form's ",[18,851,852],{},"${base}"," prefix and removes any\nthat don't match the current fingerprint. Adapters that can't\nenumerate (no list endpoint, cookie-backed, native bridges without\na list API) return ",[18,855,402],{}," — orphan cleanup degrades gracefully on\nthose backends. Keys still rotate cleanly because writes go to the\nnew fingerprint key on every schema change; the only thing missed\nis active sweep of the old key, which the consumer can do manually\nvia ",[18,858,859],{},"form.clearPersistedDraft()"," if it matters.",[23,862,864],{"id":863},"async-backends-the-flash-of-default-state","Async backends + the \"flash of default state\"",[14,866,867,868,870],{},"IndexedDB (and any async custom ",[18,869,20],{},") can't deliver a value\nin time for the first render. Users see schema defaults for one\nmicrotask, then the persisted payload swaps in.",[14,872,873,874,876,877,879,880,883],{},"For small forms where that flash is jarring, stick to ",[18,875,59],{}," or\n",[18,878,75],{},". For larger forms, gate rendering on an ",[18,881,882],{},"onMounted","\ntick or show a spinner until the first mutation settles.",[23,885,887],{"id":886},"ssr","SSR",[14,889,890],{},"Persistence is automatically skipped on the server — no reads, no\nwrites. On the client, SSR-hydrated state wins over persisted state\nif both are present.",[23,892,894],{"id":893},"see-also","See also",[896,897,898,906,913],"ul",{},[899,900,901,905],"li",{},[395,902,904],{"href":903},"\u002Fdocs\u002Frecipes\u002Fpersistence","Persistence walkthrough"," — the basics",[899,907,908,912],{},[395,909,911],{"href":910},"\u002Fdocs\u002Frecipes\u002Fpersistence-policy","Persistence policy"," — what gets stored, schema-change invalidation",[899,914,915,919],{},[395,916,918],{"href":917},"\u002Fdocs\u002Frecipes\u002Fpersistence-edge-cases","Persistence edge cases"," — imperative APIs, gotchas",[921,922,923],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":155,"searchDepth":173,"depth":173,"links":925},[926,927,928,932,933,934,935],{"id":25,"depth":173,"text":26},{"id":147,"depth":173,"text":148},{"id":307,"depth":173,"text":308,"children":929},[930],{"id":429,"depth":202,"text":931},"Removing persist: entirely",{"id":462,"depth":173,"text":463},{"id":863,"depth":173,"text":864},{"id":886,"depth":173,"text":887},{"id":893,"depth":173,"text":894},"How to pick a storage backend, configure operational options, and\nplug in a custom FormStorage adapter.","md",{},"\u002Fdocs\u002Frecipes\u002Fpersistence-backends",{"title":5,"description":936},"docs\u002Frecipes\u002Fpersistence-backends","p63WNQJqRIeZD8ekIVam2JWGrZ-IPnH1CxReGv499YM",1777934136575]