[{"data":1,"prerenderedAt":750},["ShallowReactive",2],{"content-\u002Fdocs\u002Fcross-cutting-state\u002Fmulti-tab-sync":3},{"id":4,"title":5,"body":6,"description":724,"extension":725,"meta":726,"metaRows":727,"navigation":744,"path":745,"seo":746,"source":747,"stem":748,"__hash__":749},"docs\u002Fdocs\u002Fcross-cutting-state\u002Fmulti-tab-sync.md","Multi-tab sync",{"type":7,"value":8,"toc":708},"minimark",[9,13,20,23,39,43,48,51,54,58,178,181,185,188,191,251,254,258,280,327,330,364,375,380,390,396,399,425,487,491,494,498,518,521,525,534,537,563,567,676,680,704],[10,11,5],"h1",{"id":12},"multi-tab-sync",[14,15,16],"blockquote",{},[17,18,19],"p",{},"Same-keyed forms in same-origin tabs converge live, once you opt in. Type in one, the other catches up on the next microtask.",[21,22],"docs-meta-table",{},[17,24,25,26,30,31,34,35,38],{},"Open this page in a second tab (duplicate, or a regular new tab navigating to the same URL), then type in either one. The demo's ",[27,28,29],"code",{},"useForm"," sets ",[27,32,33],{},"multiTab: true"," so the broadcaster mirrors every keystroke. Errors and submit lifecycle stay tab-local; only values and ",[27,36,37],{},"blankPaths"," cross the wire.",[40,41],"docs-demo",{"label":42,"slug":12},"Multi-tab Sync Demo",[44,45,47],"h2",{"id":46},"what-it-closes","What it closes",[17,49,50],{},"The user-impact footgun without sync: a user submits in tab A while tab B holds stale state. Tab B looks live (no error), so subsequent edits there race against and overwrite the just-submitted truth. The data-loss mode is invisible to the user.",[17,52,53],{},"With sync on, every same-keyed tab converges in near real-time. Tab B sees tab A's submit (the cleared form), so further edits there start from a known baseline.",[44,55,57],{"id":56},"what-syncs","What syncs",[59,60,61,74],"table",{},[62,63,64],"thead",{},[65,66,67,71],"tr",{},[68,69,70],"th",{},"Surface",[68,72,73],{},"Sync model",[75,76,77,92,102,112,131,146,156,164],"tbody",{},[65,78,79,85],{},[80,81,82],"td",{},[27,83,84],{},"form.values",[80,86,87,88,91],{},"Per-mutation ",[27,89,90],{},"Patch[]"," (live); full snapshot on join.",[65,93,94,99],{},[80,95,96,98],{},[27,97,37],{}," set",[80,100,101],{},"Per-mutation added\u002Fremoved; snapshot on join.",[65,103,104,109],{},[80,105,106],{},[27,107,108],{},"errors",[80,110,111],{},"NOT synced; locally re-derived from values via validation.",[65,113,114,117],{},[80,115,116],{},"Field interaction state",[80,118,119,120,123,124,123,127,130],{},"NOT synced; ",[27,121,122],{},"touched","\u002F",[27,125,126],{},"focused",[27,128,129],{},"blurred"," are UI state, tab-local.",[65,132,133,136],{},[80,134,135],{},"Submit lifecycle",[80,137,119,138,141,142,145],{},[27,139,140],{},"submissionAttempts"," \u002F ",[27,143,144],{},"submitError"," are per-callsite.",[65,147,148,153],{},[80,149,150],{},[27,151,152],{},"instanceId",[80,154,155],{},"NOT synced; per-mount identity by definition.",[65,157,158,161],{},[80,159,160],{},"History chain",[80,162,163],{},"NOT synced; each tab walks its own user's undo timeline.",[65,165,166,175],{},[80,167,168,141,171,174],{},[27,169,170],{},"File",[27,172,173],{},"Blob"," values",[80,176,177],{},"NOT synced; security + performance default-deny. See below.",[17,179,180],{},"Errors aren't broadcast because they'd carry sensitive context (\"invalid SSN: 123-45-6789\"). Each tab re-runs its own validation against the synced value: one source of truth for the data, zero leaks.",[44,182,184],{"id":183},"conflict-semantics","Conflict semantics",[17,186,187],{},"Last-writer-wins. Two tabs typing into the same field at the same instant produce convergent state on whichever message arrives later. For form fields (mostly short scalars), the cost of an occasional clobbered character is far less than the cost of invisible divergence.",[17,189,190],{},"There's no focus-skip rule; the field a user is currently in WILL accept remote writes mid-typing. For stricter semantics on a particular field, opt it out per-register:",[192,193,198],"pre",{"className":194,"code":195,"language":196,"meta":197,"style":197},"language-vue shiki shiki-themes github-light github-dark","\u003Cinput v-register=\"form.register('notes', { multiTab: false })\" \u002F>\n","vue","",[27,199,200],{"__ignoreMap":197},[201,202,205,209,213,217,220,224,227,230,233,236,239,243,246,248],"span",{"class":203,"line":204},"line",1,[201,206,208],{"class":207},"sVt8B","\u003C",[201,210,212],{"class":211},"s9eBZ","input",[201,214,216],{"class":215},"sScJk"," v-register",[201,218,219],{"class":207},"=",[201,221,223],{"class":222},"sZZnC","\"",[201,225,226],{"class":207},"form.",[201,228,229],{"class":215},"register",[201,231,232],{"class":207},"(",[201,234,235],{"class":222},"'notes'",[201,237,238],{"class":207},", { multiTab: ",[201,240,242],{"class":241},"sj4cs","false",[201,244,245],{"class":207}," })",[201,247,223],{"class":222},[201,249,250],{"class":207}," \u002F>\n",[17,252,253],{},"The opted-out field stays tab-local: broadcasts neither out nor in for that path, even when the rest of the form syncs.",[44,255,257],{"id":256},"enabling-and-disabling-sync","Enabling and disabling sync",[17,259,260,261,265,266,269,270,272,273,276,277,279],{},"Multi-tab sync is ",[262,263,264],"strong",{},"off by default",". The same opt-in cascade ",[27,267,268],{},"persist"," uses (per-form ",[27,271,29],{}," > plugin-level ",[27,274,275],{},"createAttaform({ defaults })"," > library default ",[27,278,242],{},") decides whether the broadcaster instantiates. To enable at any scope:",[192,281,285],{"className":282,"code":283,"language":284,"meta":197,"style":197},"language-ts shiki shiki-themes github-light github-dark","useForm({ key: 'signup', multiTab: true }) \u002F\u002F single form\ncreateAttaform({ defaults: { multiTab: true } }) \u002F\u002F app-wide default\n","ts",[27,286,287,310],{"__ignoreMap":197},[201,288,289,291,294,297,300,303,306],{"class":203,"line":204},[201,290,29],{"class":215},[201,292,293],{"class":207},"({ key: ",[201,295,296],{"class":222},"'signup'",[201,298,299],{"class":207},", multiTab: ",[201,301,302],{"class":241},"true",[201,304,305],{"class":207}," }) ",[201,307,309],{"class":308},"sJ8bj","\u002F\u002F single form\n",[201,311,313,316,319,321,324],{"class":203,"line":312},2,[201,314,315],{"class":215},"createAttaform",[201,317,318],{"class":207},"({ defaults: { multiTab: ",[201,320,302],{"class":241},[201,322,323],{"class":207}," } }) ",[201,325,326],{"class":308},"\u002F\u002F app-wide default\n",[17,328,329],{},"Once enabled at the form scope, individual fields can opt out:",[192,331,332],{"className":194,"code":195,"language":196,"meta":197,"style":197},[27,333,334],{"__ignoreMap":197},[201,335,336,338,340,342,344,346,348,350,352,354,356,358,360,362],{"class":203,"line":204},[201,337,208],{"class":207},[201,339,212],{"class":211},[201,341,216],{"class":215},[201,343,219],{"class":207},[201,345,223],{"class":222},[201,347,226],{"class":207},[201,349,229],{"class":215},[201,351,232],{"class":207},[201,353,235],{"class":222},[201,355,238],{"class":207},[201,357,242],{"class":241},[201,359,245],{"class":207},[201,361,223],{"class":222},[201,363,250],{"class":207},[17,365,366,367,370,371,374],{},"The cascade is most-specific-wins. A field's ",[27,368,369],{},"register({ multiTab: false })"," takes a tab-local stance even when the form is otherwise broadcasting. Form-level or plugin-level ",[27,372,373],{},"multiTab: false"," disables the module entirely; nothing instantiates.",[376,377,379],"h3",{"id":378},"why-opt-in","Why opt-in",[17,381,382,383,386,387,389],{},"Same-keyed forms broadcasting by default surprises users (a value typed in one tab appearing in another they'd forgotten) and leaks state for forms whose paths don't match the ",[27,384,385],{},"sensitiveNames"," heuristic. Pairing with ",[27,388,268],{}," (also opt-in) gives Attaform one consistent rule for state that escapes the local form scope: explicit consent. If you want sync, you say so once on the form.",[44,391,393,394],{"id":392},"pairing-with-persist","Pairing with ",[27,395,268],{},[17,397,398],{},"Sync and persistence are independent opt-ins. Both flags follow the same cascade and the same \"off by default\" stance, so adopters compose them deliberately: pick the one(s) you need.",[400,401,402,409,415],"ul",{},[403,404,405,408],"li",{},[262,406,407],{},"Sync only",": live cross-tab convergence; no durable baseline. Reloading the tab loses the in-memory state and fresh-joins via handshake to any other live tab.",[403,410,411,414],{},[262,412,413],{},"Persist only",": durable baseline; tabs don't see each other's mid-edit state.",[403,416,417,420,421,424],{},[262,418,419],{},"Both",": sync drives live convergence; persist drives warm-start. Persistence hydration is the floor; when a ",[27,422,423],{},"BroadcastChannel"," snapshot arrives on a fresh mount, it overrides the disk-persisted baseline.",[192,426,428],{"className":282,"code":427,"language":284,"meta":197,"style":197},"useForm({\n  schema,\n  key: 'signup',\n  persist: 'local', \u002F\u002F opt-in to warm-start\n  multiTab: true, \u002F\u002F opt-in to live cross-tab convergence\n})\n",[27,429,430,437,442,453,468,481],{"__ignoreMap":197},[201,431,432,434],{"class":203,"line":204},[201,433,29],{"class":215},[201,435,436],{"class":207},"({\n",[201,438,439],{"class":203,"line":312},[201,440,441],{"class":207},"  schema,\n",[201,443,445,448,450],{"class":203,"line":444},3,[201,446,447],{"class":207},"  key: ",[201,449,296],{"class":222},[201,451,452],{"class":207},",\n",[201,454,456,459,462,465],{"class":203,"line":455},4,[201,457,458],{"class":207},"  persist: ",[201,460,461],{"class":222},"'local'",[201,463,464],{"class":207},", ",[201,466,467],{"class":308},"\u002F\u002F opt-in to warm-start\n",[201,469,471,474,476,478],{"class":203,"line":470},5,[201,472,473],{"class":207},"  multiTab: ",[201,475,302],{"class":241},[201,477,464],{"class":207},[201,479,480],{"class":308},"\u002F\u002F opt-in to live cross-tab convergence\n",[201,482,484],{"class":203,"line":483},6,[201,485,486],{"class":207},"})\n",[44,488,490],{"id":489},"security","Security",[17,492,493],{},"Required reading for production deployments handling regulated data (PII, PHI, FedRAMP, HIPAA).",[376,495,497],{"id":496},"secure-context-requirement","Secure-context requirement",[17,499,500,501,504,505,464,508,464,511,464,514,517],{},"The module activates only when ",[27,502,503],{},"window.isSecureContext === true",": HTTPS in production OR localhost in development (covers ",[27,506,507],{},"localhost",[27,509,510],{},"127.0.0.1",[27,512,513],{},"[::1]",[27,515,516],{},"*.localhost","). Plain HTTP on a real hostname silently no-ops with a one-shot dev warning.",[17,519,520],{},"This is the same gate browsers apply to other sensitive APIs (clipboard, geolocation, push, web crypto subtle); no new mental model. Production deployments must be served over HTTPS for sync to function.",[376,522,524],{"id":523},"threat-model","Threat model",[17,526,527,529,530,533],{},[27,528,423],{}," is ",[262,531,532],{},"same-origin only"," (browser-enforced). Cross-origin tabs, iframes, and windows cannot subscribe. Messages are transient (not persisted); no replay-across-reload surface.",[17,535,536],{},"What enabling sync expands vs. the no-sync status quo:",[400,538,539,548,554],{},[403,540,541,544,545,547],{},[262,542,543],{},"XSS amplification."," An XSS bug in any tab can passively eavesdrop on or actively inject into every same-origin tab running the same keyed form with ",[27,546,33],{},". Same-origin trust is binary; this is irreducible at the form-library layer.",[403,549,550,553],{},[262,551,552],{},"Third-party scripts on the same origin"," (analytics, embedded widgets, ad SDKs) can subscribe to channels for forms that opted in.",[403,555,556,559,560,562],{},[262,557,558],{},"PII \u002F PHI exposure"," widens to the channel scope. Auditing which forms set ",[27,561,33],{}," is now the gate; Attaform's default (off) is the safe baseline.",[376,564,566],{"id":565},"defenses-built-in-not-optional","Defenses (built in, not optional)",[400,568,569,613,629,645,655,661],{},[403,570,571,574,575,577,578],{},[262,572,573],{},"Sensitive-path filtering, outbound AND inbound."," Paths matching the resolved ",[27,576,385],{}," list are stripped before posting AND rejected on receive. Defense in depth: the wire is never trusted, even when the originating tab \"should have\" stripped them. The same list also gates persistence opt-ins; extend per-form or globally:",[192,579,581],{"className":282,"code":580,"language":284,"meta":197,"style":197},"createAttaform({\n  defaults: { sensitiveNames: [...DEFAULT_SENSITIVE_NAMES, 'mrn'] },\n})\n",[27,582,583,589,609],{"__ignoreMap":197},[201,584,585,587],{"class":203,"line":204},[201,586,315],{"class":215},[201,588,436],{"class":207},[201,590,591,594,598,601,603,606],{"class":203,"line":312},[201,592,593],{"class":207},"  defaults: { sensitiveNames: [",[201,595,597],{"class":596},"szBVR","...",[201,599,600],{"class":241},"DEFAULT_SENSITIVE_NAMES",[201,602,464],{"class":207},[201,604,605],{"class":222},"'mrn'",[201,607,608],{"class":207},"] },\n",[201,610,611],{"class":203,"line":444},[201,612,486],{"class":207},[403,614,615,618,619,141,622,141,625,628],{},[262,616,617],{},"Prototype-pollution defense."," Inbound patches with ",[27,620,621],{},"__proto__",[27,623,624],{},"constructor",[27,626,627],{},"prototype"," segments in their path are rejected before the apply step touches the form.",[403,630,631,638,639,641,642,644],{},[262,632,633,634,637],{},"Echo drop via per-module ",[27,635,636],{},"senderId","."," Every outbound message carries a per-",[27,640,29],{}," UUID; receivers drop messages whose ",[27,643,636],{}," matches their own. Defends intra-tab self-loops and any UA echo behavior.",[403,646,647,650,651,654],{},[262,648,649],{},"Protocol versioning."," Every message carries ",[27,652,653],{},"v: 1","; unknown versions are dropped silently. Lets the wire format evolve across rolling deploys without silently corrupting older tabs.",[403,656,657,660],{},[262,658,659],{},"No errors \u002F submit lifecycle on the wire."," An error message could carry sensitive context, so it never leaves the local tab.",[403,662,663,671,672,675],{},[262,664,665,667,668,670],{},[27,666,170],{}," and ",[27,669,173],{}," values are never synced."," A user picking a sensitive file (passport scan, tax form, ID) shouldn't see that file silently broadcast to a sibling tab; that's a real disclosure surface on shared computers, in forgotten popups, or against same-origin XSS that opened a hidden tab. File blobs are also large enough that the synchronous ",[27,673,674],{},"structuredClone"," would stutter the channel. Outbound patches strip File-valued leaves, the snapshot scrubber strips File leaves from joining-tab handshakes, and inbound traffic rejects File-valued payloads (defense in depth, in case a peer ships an older bundle). If you genuinely need cross-tab file sharing, serialize to a string (base64, blob URL, server-side reference token) at a different field and accept the explicit trade-off.",[44,677,679],{"id":678},"where-to-next","Where to next",[400,681,682,690,697],{},[403,683,684,689],{},[685,686,688],"a",{"href":687},"\u002Fdocs\u002Fpersistence\u002Fsensitive-names","Sensitive-name protection",": the same list gates broadcasts and persistence.",[403,691,692,696],{},[685,693,695],{"href":694},"\u002Fdocs\u002Fpersistence\u002Fedge-cases","Persistence edge cases & hydration",": handles the warm-start case; sync handles the live case.",[403,698,699,703],{},[685,700,702],{"href":701},"\u002Fdocs\u002Fcross-cutting-state\u002Fapp-defaults","App-wide defaults",": disable sync globally or extend the sensitive-names list.",[705,706,707],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":197,"searchDepth":312,"depth":312,"links":709},[710,711,712,713,716,718,723],{"id":46,"depth":312,"text":47},{"id":56,"depth":312,"text":57},{"id":183,"depth":312,"text":184},{"id":256,"depth":312,"text":257,"children":714},[715],{"id":378,"depth":444,"text":379},{"id":392,"depth":312,"text":717},"Pairing with persist",{"id":489,"depth":312,"text":490,"children":719},[720,721,722],{"id":496,"depth":444,"text":497},{"id":523,"depth":444,"text":524},{"id":565,"depth":444,"text":566},{"id":678,"depth":312,"text":679},"Set multiTab to true on a keyed useForm and same-origin tabs sharing that key mirror each other live via BroadcastChannel. Opt-in by design, paired with the persist opt-in for a consistent \"richer state needs explicit consent\" rule.","md",{},[728,731,735,738,741],{"label":729,"value":730},"Category","Module",{"label":732,"value":733},"Opt in",{"useForm({ key, multiTab":734,"kind":27},"true })",{"label":736,"value":737,"kind":27},"Library default","multiTab false",{"label":739,"value":740},"Transport","BroadcastChannel (same-origin)",{"label":742,"value":743},"Security gate","secure context (HTTPS or localhost)",true,"\u002Fdocs\u002Fcross-cutting-state\u002Fmulti-tab-sync",{"title":5,"description":724},null,"docs\u002Fcross-cutting-state\u002Fmulti-tab-sync","8e1ObnXMYbVoy8yACdB-_PtI35Nofl8oCdZUuyJqAp8",1780949761273]