[{"data":1,"prerenderedAt":947},["ShallowReactive",2],{"content-\u002Fdocs\u002Fpersistence\u002Fstorage-backends":3},{"id":4,"title":5,"body":6,"description":926,"extension":927,"meta":928,"metaRows":929,"navigation":518,"path":942,"seo":943,"source":944,"stem":945,"__hash__":946},"docs\u002Fdocs\u002Fpersistence\u002Fstorage-backends.md","Storage backends",{"type":7,"value":8,"toc":917},"minimark",[9,13,20,23,47,51,56,127,221,247,253,260,375,378,437,463,467,486,536,547,551,557,847,857,861,880,884,913],[10,11,5],"h1",{"id":12},"storage-backends",[14,15,16],"blockquote",{},[17,18,19],"p",{},"Pick the backend that fits your fidelity, size, and lifetime needs. Attaform handles the rest, and the bundle stays lean because only the chosen backend ships.",[21,22],"docs-meta-table",{},[17,24,25,26,30,31,34,35,38,39,42,43,46],{},"This form persists to ",[27,28,29],"code",{},"'indexeddb'",". Type, refresh the page, and your draft (including the live ",[27,32,33],{},"Date"," instance on the due-date field) comes back through structured clone. Swapping to ",[27,36,37],{},"'local'"," or ",[27,40,41],{},"'session'"," is a one-word change in ",[27,44,45],{},"persist","; the bundle includes only the backend you pick.",[48,49],"docs-demo",{"label":50,"slug":12},"Storage Backends Demo",[52,53,55],"h2",{"id":54},"the-four-kinds","The four kinds",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-ts shiki shiki-themes github-light github-dark","useForm({ schema, persist: 'local' }) \u002F\u002F localStorage\nuseForm({ schema, persist: 'session' }) \u002F\u002F sessionStorage\nuseForm({ schema, persist: 'indexeddb' }) \u002F\u002F IndexedDB\nuseForm({ schema, persist: customAdapter }) \u002F\u002F any FormStorage object\n","ts","",[27,64,65,88,102,116],{"__ignoreMap":62},[66,67,70,74,78,81,84],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"sScJk","useForm",[66,75,77],{"class":76},"sVt8B","({ schema, persist: ",[66,79,37],{"class":80},"sZZnC",[66,82,83],{"class":76}," }) ",[66,85,87],{"class":86},"sJ8bj","\u002F\u002F localStorage\n",[66,89,91,93,95,97,99],{"class":68,"line":90},2,[66,92,73],{"class":72},[66,94,77],{"class":76},[66,96,41],{"class":80},[66,98,83],{"class":76},[66,100,101],{"class":86},"\u002F\u002F sessionStorage\n",[66,103,105,107,109,111,113],{"class":68,"line":104},3,[66,106,73],{"class":72},[66,108,77],{"class":76},[66,110,29],{"class":80},[66,112,83],{"class":76},[66,114,115],{"class":86},"\u002F\u002F IndexedDB\n",[66,117,119,121,124],{"class":68,"line":118},4,[66,120,73],{"class":72},[66,122,123],{"class":76},"({ schema, persist: customAdapter }) ",[66,125,126],{"class":86},"\u002F\u002F any FormStorage object\n",[128,129,130,149],"table",{},[131,132,133],"thead",{},[134,135,136,140,143,146],"tr",{},[137,138,139],"th",{},"Backend",[137,141,142],{},"Size budget",[137,144,145],{},"Sync\u002Fasync",[137,147,148],{},"Best for",[150,151,152,168,181,206],"tbody",{},[134,153,154,159,162,165],{},[155,156,157],"td",{},[27,158,37],{},[155,160,161],{},"~5 MB",[155,163,164],{},"sync",[155,166,167],{},"Small forms, widest compatibility. Shared across same-origin tabs.",[134,169,170,174,176,178],{},[155,171,172],{},[27,173,41],{},[155,175,161],{},[155,177,164],{},[155,179,180],{},"Tab-scoped scratch state. Clears when the tab closes.",[134,182,183,187,190,193],{},[155,184,185],{},[27,186,29],{},[155,188,189],{},"50%+ of disk",[155,191,192],{},"async",[155,194,195,196,198,199,198,202,205],{},"Large forms. ",[27,197,33],{}," \u002F ",[27,200,201],{},"Map",[27,203,204],{},"Set"," \u002F typed arrays round-trip verbatim.",[134,207,208,213,216,218],{},[155,209,210],{},[27,211,212],{},"FormStorage",[155,214,215],{},"you decide",[155,217,215],{},[155,219,220],{},"Encrypted stores, cookie-backed, native-mobile bridges.",[17,222,223,225,226,228,229,232,233,235,236,238,239,241,242,241,244,246],{},[27,224,37],{}," and ",[27,227,41],{}," go through ",[27,230,231],{},"JSON.stringify",", so non-JSON leaves lose fidelity (a ",[27,234,33],{}," becomes a string). ",[27,237,29],{}," uses the browser's structured-clone algorithm: ",[27,240,33],{},", ",[27,243,201],{},[27,245,204],{},", typed arrays, and nested objects ride through cleanly.",[17,248,249,250,252],{},"Only the backend you pick is bundled. Choosing ",[27,251,37],{}," doesn't pull in the IndexedDB module; the kind dispatch is dynamic-import behind the scenes.",[52,254,256,257,259],{"id":255},"full-persist-options","Full ",[27,258,45],{}," options",[57,261,263],{"className":59,"code":262,"language":61,"meta":62,"style":62},"useForm({\n  schema,\n  persist: {\n    storage: 'local' | 'session' | 'indexeddb' | customAdapter,\n    key: 'override-key', \u002F\u002F default: `attaform:${formKey}`\n    debounceMs: 500, \u002F\u002F default 300\n    include: 'form+errors', \u002F\u002F default 'form'\n    clearOnSubmitSuccess: false, \u002F\u002F default true\n  },\n})\n",[27,264,265,272,277,282,306,320,335,349,363,369],{"__ignoreMap":62},[66,266,267,269],{"class":68,"line":69},[66,268,73],{"class":72},[66,270,271],{"class":76},"({\n",[66,273,274],{"class":68,"line":90},[66,275,276],{"class":76},"  schema,\n",[66,278,279],{"class":68,"line":104},[66,280,281],{"class":76},"  persist: {\n",[66,283,284,287,289,293,296,298,301,303],{"class":68,"line":118},[66,285,286],{"class":76},"    storage: ",[66,288,37],{"class":80},[66,290,292],{"class":291},"szBVR"," |",[66,294,295],{"class":80}," 'session'",[66,297,292],{"class":291},[66,299,300],{"class":80}," 'indexeddb'",[66,302,292],{"class":291},[66,304,305],{"class":76}," customAdapter,\n",[66,307,309,312,315,317],{"class":68,"line":308},5,[66,310,311],{"class":76},"    key: ",[66,313,314],{"class":80},"'override-key'",[66,316,241],{"class":76},[66,318,319],{"class":86},"\u002F\u002F default: `attaform:${formKey}`\n",[66,321,323,326,330,332],{"class":68,"line":322},6,[66,324,325],{"class":76},"    debounceMs: ",[66,327,329],{"class":328},"sj4cs","500",[66,331,241],{"class":76},[66,333,334],{"class":86},"\u002F\u002F default 300\n",[66,336,338,341,344,346],{"class":68,"line":337},7,[66,339,340],{"class":76},"    include: ",[66,342,343],{"class":80},"'form+errors'",[66,345,241],{"class":76},[66,347,348],{"class":86},"\u002F\u002F default 'form'\n",[66,350,352,355,358,360],{"class":68,"line":351},8,[66,353,354],{"class":76},"    clearOnSubmitSuccess: ",[66,356,357],{"class":328},"false",[66,359,241],{"class":76},[66,361,362],{"class":86},"\u002F\u002F default true\n",[66,364,366],{"class":68,"line":365},9,[66,367,368],{"class":76},"  },\n",[66,370,372],{"class":68,"line":371},10,[66,373,374],{"class":76},"})\n",[17,376,377],{},"Five operational knobs:",[379,380,381,388,398,408,421],"ul",{},[382,383,384,387],"li",{},[27,385,386],{},"storage",": the backend (same union as the shorthand).",[382,389,390,393,394,397],{},[27,391,392],{},"key",": overrides the default ",[27,395,396],{},"attaform:${formKey}"," prefix. The schema fingerprint is appended automatically; you supply only the human-readable label.",[382,399,400,403,404,407],{},[27,401,402],{},"debounceMs",": coalesces typing bursts into one write. Drop it to ",[27,405,406],{},"0"," for save-every-keystroke feedback, raise it for slower backends.",[382,409,410,413,414,417,418,420],{},[27,411,412],{},"include",": ",[27,415,416],{},"'form'"," persists values only; ",[27,419,343],{}," also persists the error map (useful for multi-step wizards that don't want to re-run server validation on reload).",[382,422,423,413,426,429,430,433,434,436],{},[27,424,425],{},"clearOnSubmitSuccess",[27,427,428],{},"true"," wipes the draft when ",[27,431,432],{},"handleSubmit","'s success callback resolves; ",[27,435,357],{}," keeps it (review pages, retry-prone APIs).",[17,438,439,440,443,444,447,448,451,452,457,458,462],{},"What's NOT here: an allowlist \u002F blocklist of paths, a ",[27,441,442],{},"redactFields"," knob, a ",[27,445,446],{},"version:"," field. Per-field opt-in lives on each ",[27,449,450],{},"register"," call (see ",[453,454,456],"a",{"href":455},"\u002Fdocs\u002Fpersistence\u002Fper-field-opt-in","Per-field opt-in","); schema-change invalidation flows from the fingerprint (see ",[453,459,461],{"href":460},"\u002Fdocs\u002Fpersistence\u002Fedge-cases","Edge cases & hydration",").",[52,464,466],{"id":465},"switching-backends-safely","Switching backends safely",[17,468,469,470,472,473,241,475,241,477,479,480,482,483,485],{},"The configured ",[27,471,386],{}," is the source of truth for \"where the draft lives now.\" On every mount, an orphan-cleanup pass sweeps the three standard backends (",[27,474,37],{},[27,476,41],{},[27,478,29],{},") under the form's key prefix and removes anything that doesn't match the configured backend's current-fingerprint entry. So switching from ",[27,481,37],{}," to ",[27,484,41],{}," (or to a custom encrypted adapter) can't leave a stale PII envelope behind.",[57,487,489],{"className":59,"code":488,"language":61,"meta":62,"style":62},"\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",[27,490,491,496,514,520,525],{"__ignoreMap":62},[66,492,493],{"class":68,"line":69},[66,494,495],{"class":86},"\u002F\u002F Before:\n",[66,497,498,500,503,506,509,511],{"class":68,"line":90},[66,499,73],{"class":72},[66,501,502],{"class":76},"({ schema, key: ",[66,504,505],{"class":80},"'signup'",[66,507,508],{"class":76},", persist: ",[66,510,37],{"class":80},[66,512,513],{"class":76}," })\n",[66,515,516],{"class":68,"line":104},[66,517,519],{"emptyLinePlaceholder":518},true,"\n",[66,521,522],{"class":68,"line":118},[66,523,524],{"class":86},"\u002F\u002F After (next deploy): mount-time sweep wipes the old 'local' entry.\n",[66,526,527,529,531,533],{"class":68,"line":308},[66,528,73],{"class":72},[66,530,502],{"class":76},[66,532,505],{"class":80},[66,534,535],{"class":76},", persist: encryptedStorage })\n",[17,537,538,539,542,543,546],{},"Removing ",[27,540,541],{},"persist:"," entirely sweeps all three standard backends for the form's default key, so disabling persistence actually clears the on-disk artifact instead of leaving it indefinitely. The only thing the sweep can't reach is a custom ",[27,544,545],{},"persist.key"," from a previous deployment; rename migrations are an explicit consumer move.",[52,548,550],{"id":549},"custom-backend","Custom backend",[17,552,553,554,556],{},"Implement the four-method ",[27,555,212],{}," interface and pass the object directly:",[57,558,560],{"className":59,"code":559,"language":61,"meta":62,"style":62},"import type { FormStorage } from 'attaform'\n\nconst encryptedStorage: FormStorage = {\n  async getItem(key) {\n    return await fetch(`\u002Fapi\u002Fdrafts\u002F${key}`).then((r) => r.json())\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    return await fetch(`\u002Fapi\u002Fdrafts?prefix=${prefix}`).then((r) => r.json())\n  },\n}\n\nuseForm({ schema, key: 'signup', persist: encryptedStorage })\n",[27,561,562,579,583,603,620,667,671,689,725,729,742,764,769,784,820,825,831,836],{"__ignoreMap":62},[66,563,564,567,570,573,576],{"class":68,"line":69},[66,565,566],{"class":291},"import",[66,568,569],{"class":291}," type",[66,571,572],{"class":76}," { FormStorage } ",[66,574,575],{"class":291},"from",[66,577,578],{"class":80}," 'attaform'\n",[66,580,581],{"class":68,"line":90},[66,582,519],{"emptyLinePlaceholder":518},[66,584,585,588,591,594,597,600],{"class":68,"line":104},[66,586,587],{"class":291},"const",[66,589,590],{"class":328}," encryptedStorage",[66,592,593],{"class":291},":",[66,595,596],{"class":72}," FormStorage",[66,598,599],{"class":291}," =",[66,601,602],{"class":76}," {\n",[66,604,605,608,611,614,617],{"class":68,"line":118},[66,606,607],{"class":291},"  async",[66,609,610],{"class":72}," getItem",[66,612,613],{"class":76},"(",[66,615,392],{"class":616},"s4XuR",[66,618,619],{"class":76},") {\n",[66,621,622,625,628,631,633,636,638,641,643,646,649,652,655,658,661,664],{"class":68,"line":308},[66,623,624],{"class":291},"    return",[66,626,627],{"class":291}," await",[66,629,630],{"class":72}," fetch",[66,632,613],{"class":76},[66,634,635],{"class":80},"`\u002Fapi\u002Fdrafts\u002F${",[66,637,392],{"class":76},[66,639,640],{"class":80},"}`",[66,642,462],{"class":76},[66,644,645],{"class":72},"then",[66,647,648],{"class":76},"((",[66,650,651],{"class":616},"r",[66,653,654],{"class":76},") ",[66,656,657],{"class":291},"=>",[66,659,660],{"class":76}," r.",[66,662,663],{"class":72},"json",[66,665,666],{"class":76},"())\n",[66,668,669],{"class":68,"line":322},[66,670,368],{"class":76},[66,672,673,675,678,680,682,684,687],{"class":68,"line":337},[66,674,607],{"class":291},[66,676,677],{"class":72}," setItem",[66,679,613],{"class":76},[66,681,392],{"class":616},[66,683,241],{"class":76},[66,685,686],{"class":616},"value",[66,688,619],{"class":76},[66,690,691,694,696,698,700,702,704,707,710,713,716,719,722],{"class":68,"line":351},[66,692,693],{"class":291},"    await",[66,695,630],{"class":72},[66,697,613],{"class":76},[66,699,635],{"class":80},[66,701,392],{"class":76},[66,703,640],{"class":80},[66,705,706],{"class":76},", { method: ",[66,708,709],{"class":80},"'PUT'",[66,711,712],{"class":76},", body: ",[66,714,715],{"class":328},"JSON",[66,717,718],{"class":76},".",[66,720,721],{"class":72},"stringify",[66,723,724],{"class":76},"(value) })\n",[66,726,727],{"class":68,"line":365},[66,728,368],{"class":76},[66,730,731,733,736,738,740],{"class":68,"line":371},[66,732,607],{"class":291},[66,734,735],{"class":72}," removeItem",[66,737,613],{"class":76},[66,739,392],{"class":616},[66,741,619],{"class":76},[66,743,745,747,749,751,753,755,757,759,762],{"class":68,"line":744},11,[66,746,693],{"class":291},[66,748,630],{"class":72},[66,750,613],{"class":76},[66,752,635],{"class":80},[66,754,392],{"class":76},[66,756,640],{"class":80},[66,758,706],{"class":76},[66,760,761],{"class":80},"'DELETE'",[66,763,513],{"class":76},[66,765,767],{"class":68,"line":766},12,[66,768,368],{"class":76},[66,770,772,774,777,779,782],{"class":68,"line":771},13,[66,773,607],{"class":291},[66,775,776],{"class":72}," listKeys",[66,778,613],{"class":76},[66,780,781],{"class":616},"prefix",[66,783,619],{"class":76},[66,785,787,789,791,793,795,798,800,802,804,806,808,810,812,814,816,818],{"class":68,"line":786},14,[66,788,624],{"class":291},[66,790,627],{"class":291},[66,792,630],{"class":72},[66,794,613],{"class":76},[66,796,797],{"class":80},"`\u002Fapi\u002Fdrafts?prefix=${",[66,799,781],{"class":76},[66,801,640],{"class":80},[66,803,462],{"class":76},[66,805,645],{"class":72},[66,807,648],{"class":76},[66,809,651],{"class":616},[66,811,654],{"class":76},[66,813,657],{"class":291},[66,815,660],{"class":76},[66,817,663],{"class":72},[66,819,666],{"class":76},[66,821,823],{"class":68,"line":822},15,[66,824,368],{"class":76},[66,826,828],{"class":68,"line":827},16,[66,829,830],{"class":76},"}\n",[66,832,834],{"class":68,"line":833},17,[66,835,519],{"emptyLinePlaceholder":518},[66,837,839,841,843,845],{"class":68,"line":838},18,[66,840,73],{"class":72},[66,842,502],{"class":76},[66,844,505],{"class":80},[66,846,535],{"class":76},[17,848,849,852,853,856],{},[27,850,851],{},"listKeys(prefix)"," powers the orphan sweep. Adapters that can't enumerate (HTTP-backed, cookie-backed) return ",[27,854,855],{},"[]",", and the sweep degrades gracefully without blocking the rest of the lifecycle.",[52,858,860],{"id":859},"latency-in-practice","Latency in practice",[17,862,863,864,225,866,868,869,241,873,875,876,879],{},"Write latency for a 100-leaf form: ",[27,865,37],{},[27,867,41],{}," land in ",[870,871,872],"strong",{},"~3 µs",[27,874,29],{}," in ",[870,877,878],{},"~62 µs",". Both are well under one frame; the gap matters only when you're paying for many writes per second or when async hydration timing affects first paint. For most forms, pick the backend that fits the data fidelity you need and the size budget; performance is a non-issue.",[52,881,883],{"id":882},"where-to-next","Where to next",[379,885,886,891,908],{},[382,887,888,890],{},[453,889,456],{"href":455},": the second of the two gates.",[382,892,893,897,898,198,901,198,904,907],{},[453,894,896],{"href":895},"\u002Fdocs\u002Fpersistence\u002Fsensitive-names","Sensitive-name protection",": the heuristic that blocks ",[27,899,900],{},"password",[27,902,903],{},"cvv",[27,905,906],{},"ssn"," from landing in any backend.",[382,909,910,912],{},[453,911,461],{"href":460},": fingerprint invalidation, cross-tab races, hydration ordering.",[914,915,916],"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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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":62,"searchDepth":90,"depth":90,"links":918},[919,920,922,923,924,925],{"id":54,"depth":90,"text":55},{"id":255,"depth":90,"text":921},"Full persist options",{"id":465,"depth":90,"text":466},{"id":549,"depth":90,"text":550},{"id":859,"depth":90,"text":860},{"id":882,"depth":90,"text":883},"Pick local, session, indexeddb, or a custom FormStorage adapter. The bundle ships only the backend you pick, and IndexedDB round-trips Date \u002F Map \u002F Set verbatim through structured clone.","md",{},[930,933,936,939],{"label":931,"value":932},"Category","Module",{"label":934,"value":935,"kind":27},"Built-in kinds","local · session · indexeddb",{"label":937,"value":938,"kind":27},"Custom","FormStorage interface",{"label":940,"value":941},"Bundle cost","only the picked backend ships","\u002Fdocs\u002Fpersistence\u002Fstorage-backends",{"title":5,"description":926},null,"docs\u002Fpersistence\u002Fstorage-backends","ekWx03kd276yGhJq4c66EIvgw9A796QQkvJC3yTnwxo",1780949760843]